mirror of
https://github.com/BoostIo/Boostnote
synced 2025-12-13 17:56:25 +00:00
Compare commits
167 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
789926bc76 | ||
|
|
006795b4d0 | ||
|
|
3e5d78d322 | ||
|
|
58c4a78be1 | ||
|
|
2603dfc1ed | ||
|
|
2df590600b | ||
|
|
ef20a8f3e5 | ||
|
|
3e405e1abf | ||
|
|
553832bdfa | ||
|
|
18d65d999a | ||
|
|
3b5eff582a | ||
|
|
85d09b3b3d | ||
|
|
8958e67fcf | ||
|
|
47b796909a | ||
|
|
67d76abdfa | ||
|
|
d75d68ba72 | ||
|
|
323be6b72d | ||
|
|
031a113338 | ||
|
|
b50c5386a6 | ||
|
|
65777b1d56 | ||
|
|
fe728874ac | ||
|
|
c5b4c327fa | ||
|
|
4c39922ead | ||
|
|
1cdc74a2f0 | ||
|
|
6213a820e6 | ||
|
|
ce81b26d1d | ||
|
|
fae91255f9 | ||
|
|
a82a3efb14 | ||
|
|
9556417447 | ||
|
|
60fbb7db5d | ||
|
|
e504f8e63e | ||
|
|
071ce12a7e | ||
|
|
0decaf187c | ||
|
|
961644747e | ||
|
|
d1a81984fb | ||
|
|
bd9b1306b1 | ||
|
|
0ca18d8ca5 | ||
|
|
540d72696c | ||
|
|
87a530612f | ||
|
|
7f3fdedb5d | ||
|
|
3a80706938 | ||
|
|
f4259bb4d0 | ||
|
|
aa8b589569 | ||
|
|
febc98c101 | ||
|
|
b678c3bd89 | ||
|
|
80b8948433 | ||
|
|
5414fe3384 | ||
|
|
db4016385d | ||
|
|
2cf5f8e966 | ||
|
|
8ede1a4989 | ||
|
|
76da76ae76 | ||
|
|
c02ab033f4 | ||
|
|
1aaba74e24 | ||
|
|
6fe6794796 | ||
|
|
fd3e243855 | ||
|
|
938b075bf6 | ||
|
|
81ac3d1748 | ||
|
|
40d10eae04 | ||
|
|
9b6a61a91c | ||
|
|
7116c305ca | ||
|
|
4fbbb4651d | ||
|
|
2ac38e9644 | ||
|
|
98d4fa0603 | ||
|
|
2ea0514bbe | ||
|
|
137aa692bc | ||
|
|
634fec39c0 | ||
|
|
d269f1e8fd | ||
|
|
4b67026bbf | ||
|
|
8b13ec4f0e | ||
|
|
7fef7660e4 | ||
|
|
01d021cc4c | ||
|
|
c355f81525 | ||
|
|
d138a54dfd | ||
|
|
514d4b9059 | ||
|
|
a7ead67c2d | ||
|
|
2f16784a20 | ||
|
|
8ca3ba21ee | ||
|
|
58ae6419f0 | ||
|
|
b56e0b98e3 | ||
|
|
4def32ab13 | ||
|
|
0de78d12ef | ||
|
|
e756534db4 | ||
|
|
2194965dc4 | ||
|
|
f9e54bcbfc | ||
|
|
667fd3a601 | ||
|
|
461e24bf39 | ||
|
|
ac2cfe5169 | ||
|
|
3f320f4337 | ||
|
|
433ee9ed45 | ||
|
|
6ee92588b1 | ||
|
|
0d797ce8a8 | ||
|
|
4915c545d9 | ||
|
|
e1c95fb1f2 | ||
|
|
5f56d3e0de | ||
|
|
d6b86b902c | ||
|
|
3abc0fec38 | ||
|
|
c0619eb746 | ||
|
|
791ababe1e | ||
|
|
d829216c8d | ||
|
|
1cf6f3b1e2 | ||
|
|
4d5939aaf4 | ||
|
|
2695f62f3e | ||
|
|
ccd0355d0b | ||
|
|
6d6e3a51c0 | ||
|
|
48c8164689 | ||
|
|
38ed5b8541 | ||
|
|
8a6df8bf95 | ||
|
|
9c3f34fe04 | ||
|
|
d4123eeccd | ||
|
|
b91a76b3b1 | ||
|
|
fc08d2f8c3 | ||
|
|
59e361cb37 | ||
|
|
1993a6588d | ||
|
|
218fba1aa1 | ||
|
|
4de6c69f5d | ||
|
|
2b3538d3b1 | ||
|
|
b84f1173b7 | ||
|
|
bdfe8c0445 | ||
|
|
f64d0b35e1 | ||
|
|
3921655157 | ||
|
|
e4e10d523f | ||
|
|
404dddcb86 | ||
|
|
ffb2603485 | ||
|
|
928e0edf4d | ||
|
|
80a63f7404 | ||
|
|
6e45ee6a38 | ||
|
|
ba34458feb | ||
|
|
a2fb50a71c | ||
|
|
b15a4007ee | ||
|
|
93f0d3c1cf | ||
|
|
8ec7d19f30 | ||
|
|
a0f5a06c73 | ||
|
|
39a98e795f | ||
|
|
57705cf41b | ||
|
|
052c70bb38 | ||
|
|
6dc88262c9 | ||
|
|
9d43e34cfa | ||
|
|
12f9b9342d | ||
|
|
e76bc72667 | ||
|
|
9310e5e86c | ||
|
|
fa157f6f76 | ||
|
|
d6a54b8a26 | ||
|
|
9813412c8e | ||
|
|
d76b7235db | ||
|
|
418a789568 | ||
|
|
a19ff6762e | ||
|
|
2d941c3ea3 | ||
|
|
7034e7b620 | ||
|
|
cd53a65c14 | ||
|
|
8b54f5aa69 | ||
|
|
ceed178061 | ||
|
|
33662974bf | ||
|
|
36b97fc6a2 | ||
|
|
540c608cc6 | ||
|
|
0f8c627474 | ||
|
|
f38fef23a0 | ||
|
|
8be0ea64a5 | ||
|
|
1419c71ef5 | ||
|
|
e13742445e | ||
|
|
1d21bb1ea3 | ||
|
|
aa38b1f859 | ||
|
|
e723d4cd59 | ||
|
|
9e770ef357 | ||
|
|
c796b3b30e | ||
|
|
168fe212f5 | ||
|
|
87515dbd3f | ||
|
|
696c2f29b5 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -11,3 +11,4 @@ node_modules/*
|
||||
.idea
|
||||
.vscode
|
||||
package-lock.json
|
||||
config.json
|
||||
|
||||
@@ -21,6 +21,8 @@ const buildEditorContextMenu = require('browser/lib/contextMenuBuilder')
|
||||
import { createTurndownService } from '../lib/turndown'
|
||||
import { languageMaps } from '../lib/CMLanguageList'
|
||||
import snippetManager from '../lib/SnippetManager'
|
||||
import { findStorage } from 'browser/lib/findStorage'
|
||||
import { sendWakatimeHeartBeat } from 'browser/lib/wakatime-plugin'
|
||||
import {
|
||||
generateInEditor,
|
||||
tocExistsInEditor
|
||||
@@ -61,7 +63,7 @@ export default class CodeEditor extends React.Component {
|
||||
this.focusHandler = () => {
|
||||
ipcRenderer.send('editor:focused', true)
|
||||
}
|
||||
const debouncedDeletionOfAttachments = _.debounce(
|
||||
this.debouncedDeletionOfAttachments = _.debounce(
|
||||
attachmentManagement.deleteAttachmentsNotPresentInNote,
|
||||
30000
|
||||
)
|
||||
@@ -78,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) {
|
||||
debouncedDeletionOfAttachments(
|
||||
this.debouncedDeletionOfAttachments(
|
||||
this.editor.getValue(),
|
||||
storageKey,
|
||||
noteKey
|
||||
@@ -113,6 +115,16 @@ export default class CodeEditor extends React.Component {
|
||||
this.editorActivityHandler = () => this.handleEditorActivity()
|
||||
|
||||
this.turndownService = createTurndownService()
|
||||
|
||||
// wakatime
|
||||
const { storageKey, noteKey } = this.props
|
||||
const storage = findStorage(storageKey)
|
||||
if (storage)
|
||||
sendWakatimeHeartBeat(storage.path, noteKey, storage.name, {
|
||||
isWrite: false,
|
||||
hasFileChanges: false,
|
||||
isFileChange: true
|
||||
})
|
||||
}
|
||||
|
||||
handleSearch(msg) {
|
||||
@@ -158,6 +170,10 @@ export default class CodeEditor extends React.Component {
|
||||
}
|
||||
|
||||
handleEditorActivity() {
|
||||
if (this.props.onCursorActivity) {
|
||||
this.props.onCursorActivity(this.editor)
|
||||
}
|
||||
|
||||
if (!this.textEditorInterface.transaction) {
|
||||
this.updateTableEditorState()
|
||||
}
|
||||
@@ -219,11 +235,19 @@ export default class CodeEditor extends React.Component {
|
||||
},
|
||||
[translateHotkey(hotkey.insertDate)]: function(cm) {
|
||||
const dateNow = new Date()
|
||||
cm.replaceSelection(dateNow.toLocaleDateString())
|
||||
if (self.props.dateFormatISO8601) {
|
||||
cm.replaceSelection(dateNow.toISOString().split('T')[0])
|
||||
} else {
|
||||
cm.replaceSelection(dateNow.toLocaleDateString())
|
||||
}
|
||||
},
|
||||
[translateHotkey(hotkey.insertDateTime)]: function(cm) {
|
||||
const dateNow = new Date()
|
||||
cm.replaceSelection(dateNow.toLocaleString())
|
||||
if (self.props.dateFormatISO8601) {
|
||||
cm.replaceSelection(dateNow.toISOString())
|
||||
} else {
|
||||
cm.replaceSelection(dateNow.toLocaleString())
|
||||
}
|
||||
},
|
||||
Enter: 'boostNewLineAndIndentContinueMarkdownList',
|
||||
'Ctrl-C': cm => {
|
||||
@@ -321,10 +345,18 @@ export default class CodeEditor extends React.Component {
|
||||
'CodeMirror-lint-markers'
|
||||
],
|
||||
autoCloseBrackets: {
|
||||
pairs: this.props.matchingPairs,
|
||||
triples: this.props.matchingTriples,
|
||||
explode: this.props.explodingPairs,
|
||||
override: true
|
||||
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
|
||||
}
|
||||
},
|
||||
extraKeys: this.defaultKeyMap,
|
||||
prettierConfig: this.props.prettierConfig
|
||||
@@ -352,6 +384,7 @@ 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)
|
||||
@@ -489,7 +522,6 @@ export default class CodeEditor extends React.Component {
|
||||
})
|
||||
|
||||
if (this.props.enableTableEditor) {
|
||||
this.editor.on('cursorActivity', this.editorActivityHandler)
|
||||
this.editor.on('changes', this.editorActivityHandler)
|
||||
}
|
||||
|
||||
@@ -548,12 +580,18 @@ 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) {
|
||||
@@ -629,16 +667,32 @@ 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.explodingPairs !== this.props.explodingPairs ||
|
||||
prevProps.codeBlockMatchingPairs !== this.props.codeBlockMatchingPairs ||
|
||||
prevProps.codeBlockMatchingCloseBefore !==
|
||||
this.props.codeBlockMatchingCloseBefore ||
|
||||
prevProps.codeBlockMatchingTriples !==
|
||||
this.props.codeBlockMatchingTriples ||
|
||||
prevProps.codeBlockExplodingPairs !== this.props.codeBlockExplodingPairs
|
||||
) {
|
||||
const bracketObject = {
|
||||
pairs: this.props.matchingPairs,
|
||||
triples: this.props.matchingTriples,
|
||||
explode: this.props.explodingPairs,
|
||||
override: true
|
||||
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
|
||||
}
|
||||
}
|
||||
this.editor.setOption('autoCloseBrackets', bracketObject)
|
||||
|
||||
this.editor.setOption('autoCloseBrackets', autoCloseBrackets)
|
||||
}
|
||||
|
||||
if (prevProps.enableTableEditor !== this.props.enableTableEditor) {
|
||||
@@ -756,6 +810,8 @@ 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.
|
||||
@@ -793,9 +849,23 @@ export default class CodeEditor extends React.Component {
|
||||
this.updateHighlight(editor, changeObject)
|
||||
|
||||
this.value = editor.getValue()
|
||||
|
||||
const { storageKey, noteKey } = this.props
|
||||
const storage = findStorage(storageKey)
|
||||
if (this.props.onChange) {
|
||||
this.props.onChange(editor)
|
||||
}
|
||||
|
||||
const isWrite = !!this.props.onChange
|
||||
const hasFileChanges = isWrite
|
||||
|
||||
if (storage) {
|
||||
sendWakatimeHeartBeat(storage.path, noteKey, storage.name, {
|
||||
isWrite,
|
||||
hasFileChanges,
|
||||
isFileChange: false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
linePossibleContainsHeadline(currentLine) {
|
||||
@@ -923,6 +993,16 @@ export default class CodeEditor extends React.Component {
|
||||
this.restartHighlighting()
|
||||
this.editor.on('change', this.changeHandler)
|
||||
this.editor.refresh()
|
||||
|
||||
// wakatime
|
||||
const { storageKey, noteKey } = this.props
|
||||
const storage = findStorage(storageKey)
|
||||
if (storage)
|
||||
sendWakatimeHeartBeat(storage.path, noteKey, storage.name, {
|
||||
isWrite: false,
|
||||
hasFileChanges: false,
|
||||
isFileChange: true
|
||||
})
|
||||
}
|
||||
|
||||
setValue(value) {
|
||||
@@ -1240,18 +1320,19 @@ export default class CodeEditor extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { className, fontSize } = this.props
|
||||
const fontFamily = normalizeEditorFontFamily(this.props.fontFamily)
|
||||
const width = this.props.width
|
||||
const { className, fontSize, fontFamily, width, height } = this.props
|
||||
const normalisedFontFamily = normalizeEditorFontFamily(fontFamily)
|
||||
|
||||
return (
|
||||
<div
|
||||
className={className == null ? 'CodeEditor' : `CodeEditor ${className}`}
|
||||
ref='root'
|
||||
tabIndex='-1'
|
||||
style={{
|
||||
fontFamily,
|
||||
fontSize: fontSize,
|
||||
width: width
|
||||
fontFamily: normalisedFontFamily,
|
||||
fontSize,
|
||||
width,
|
||||
height
|
||||
}}
|
||||
onDrop={e => this.handleDropImage(e)}
|
||||
/>
|
||||
|
||||
@@ -139,7 +139,7 @@ class MarkdownEditor extends React.Component {
|
||||
},
|
||||
() => {
|
||||
this.previewRef.current.focus()
|
||||
this.previewRef.current.scrollToRow(cursorPosition.line)
|
||||
this.previewRef.current.scrollToLine(cursorPosition.line)
|
||||
}
|
||||
)
|
||||
eventEmitter.emit('topbar:togglelockbutton', this.state.status)
|
||||
@@ -323,6 +323,7 @@ class MarkdownEditor extends React.Component {
|
||||
storageKey,
|
||||
noteKey,
|
||||
linesHighlighted,
|
||||
getNote,
|
||||
RTL
|
||||
} = this.props
|
||||
|
||||
@@ -365,8 +366,15 @@ 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}
|
||||
@@ -381,6 +389,7 @@ 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}
|
||||
@@ -418,6 +427,8 @@ 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,183 +18,30 @@ 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 { escapeHtmlCharacters } from 'browser/lib/utils'
|
||||
import formatMarkdown from 'browser/main/lib/dataApi/formatMarkdown'
|
||||
import formatHTML, {
|
||||
CSS_FILES,
|
||||
buildStyle,
|
||||
getCodeThemeLink,
|
||||
getStyleParams,
|
||||
escapeHtmlCharactersInCodeTag
|
||||
} from 'browser/main/lib/dataApi/formatHTML'
|
||||
import formatPDF from 'browser/main/lib/dataApi/formatPDF'
|
||||
import yaml from 'js-yaml'
|
||||
import 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 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')
|
||||
import { buildMarkdownPreviewContextMenu } from 'browser/lib/contextMenuBuilder'
|
||||
|
||||
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;
|
||||
}
|
||||
.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;'}
|
||||
@@ -226,22 +73,6 @@ const scrollBarDarkStyle = `
|
||||
}
|
||||
`
|
||||
|
||||
const OSX = global.process.platform === 'darwin'
|
||||
|
||||
const defaultFontFamily = ['helvetica', 'arial', 'sans-serif']
|
||||
if (!OSX) {
|
||||
defaultFontFamily.unshift('Microsoft YaHei')
|
||||
defaultFontFamily.unshift('meiryo')
|
||||
}
|
||||
const defaultCodeBlockFontFamily = [
|
||||
'Monaco',
|
||||
'Menlo',
|
||||
'Ubuntu Mono',
|
||||
'Consolas',
|
||||
'source-code-pro',
|
||||
'monospace'
|
||||
]
|
||||
|
||||
// return the line number of the line that used to generate the specified element
|
||||
// return -1 if the line is not found
|
||||
function getSourceLineNumberByElement(element) {
|
||||
@@ -355,94 +186,15 @@ class MarkdownPreview extends React.Component {
|
||||
}
|
||||
|
||||
handleSaveAsMd() {
|
||||
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>`
|
||||
this.exportAsDocument('md', formatMarkdown(this.props))
|
||||
}
|
||||
|
||||
handleSaveAsHtml() {
|
||||
this.exportAsDocument('html', (noteContent, exportTasks, targetDir) =>
|
||||
Promise.resolve(
|
||||
this.htmlContentFormatter(noteContent, exportTasks, targetDir)
|
||||
)
|
||||
)
|
||||
this.exportAsDocument('html', formatHTML(this.props))
|
||||
}
|
||||
|
||||
handleSaveAsPdf() {
|
||||
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()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
this.exportAsDocument('pdf', formatPDF(this.props))
|
||||
}
|
||||
|
||||
handlePrint() {
|
||||
@@ -450,18 +202,21 @@ class MarkdownPreview extends React.Component {
|
||||
}
|
||||
|
||||
exportAsDocument(fileType, contentFormatter) {
|
||||
const note = this.props.getNote()
|
||||
|
||||
const options = {
|
||||
defaultPath: filenamify(note.title, {
|
||||
replacement: '_'
|
||||
}),
|
||||
filters: [{ name: 'Documents', extensions: [fileType] }],
|
||||
properties: ['openFile', 'createDirectory']
|
||||
}
|
||||
|
||||
dialog.showSaveDialog(remote.getCurrentWindow(), options, filename => {
|
||||
if (filename) {
|
||||
const content = this.props.value
|
||||
const storage = this.props.storagePath
|
||||
const nodeKey = this.props.noteKey
|
||||
const storagePath = this.props.storagePath
|
||||
|
||||
exportNote(nodeKey, storage, content, filename, contentFormatter)
|
||||
exportNote(storagePath, note, filename, contentFormatter)
|
||||
.then(res => {
|
||||
dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
type: 'info',
|
||||
@@ -492,32 +247,6 @@ class MarkdownPreview extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Convert special characters between three ```
|
||||
* @param {string[]} splitWithCodeTag Array of HTML strings separated by three ```
|
||||
* @returns {string} HTML in which special characters between three ``` have been converted
|
||||
*/
|
||||
escapeHtmlCharactersInCodeTag(splitWithCodeTag) {
|
||||
for (let index = 0; index < splitWithCodeTag.length; index++) {
|
||||
const codeTagRequired =
|
||||
splitWithCodeTag[index] !== '```' && index < splitWithCodeTag.length - 1
|
||||
if (codeTagRequired) {
|
||||
splitWithCodeTag.splice(index + 1, 0, '```')
|
||||
}
|
||||
}
|
||||
let inCodeTag = false
|
||||
let result = ''
|
||||
for (let content of splitWithCodeTag) {
|
||||
if (content === '```') {
|
||||
inCodeTag = !inCodeTag
|
||||
} else if (inCodeTag) {
|
||||
content = escapeHtmlCharacters(content)
|
||||
}
|
||||
result += content
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
getScrollBarStyle() {
|
||||
const { theme } = this.props
|
||||
|
||||
@@ -668,47 +397,6 @@ class MarkdownPreview extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
getStyleParams() {
|
||||
const {
|
||||
fontSize,
|
||||
lineNumber,
|
||||
codeBlockTheme,
|
||||
scrollPastEnd,
|
||||
theme,
|
||||
allowCustomCSS,
|
||||
customCSS,
|
||||
RTL
|
||||
} = this.props
|
||||
let { fontFamily, codeBlockFontFamily } = this.props
|
||||
fontFamily =
|
||||
_.isString(fontFamily) && fontFamily.trim().length > 0
|
||||
? fontFamily
|
||||
.split(',')
|
||||
.map(fontName => fontName.trim())
|
||||
.concat(defaultFontFamily)
|
||||
: defaultFontFamily
|
||||
codeBlockFontFamily =
|
||||
_.isString(codeBlockFontFamily) && codeBlockFontFamily.trim().length > 0
|
||||
? codeBlockFontFamily
|
||||
.split(',')
|
||||
.map(fontName => fontName.trim())
|
||||
.concat(defaultCodeBlockFontFamily)
|
||||
: defaultCodeBlockFontFamily
|
||||
|
||||
return {
|
||||
fontFamily,
|
||||
fontSize,
|
||||
codeBlockFontFamily,
|
||||
lineNumber,
|
||||
codeBlockTheme,
|
||||
scrollPastEnd,
|
||||
theme,
|
||||
allowCustomCSS,
|
||||
customCSS,
|
||||
RTL
|
||||
}
|
||||
}
|
||||
|
||||
applyStyle() {
|
||||
const {
|
||||
fontFamily,
|
||||
@@ -721,12 +409,13 @@ class MarkdownPreview extends React.Component {
|
||||
allowCustomCSS,
|
||||
customCSS,
|
||||
RTL
|
||||
} = this.getStyleParams()
|
||||
} = getStyleParams(this.props)
|
||||
|
||||
this.getWindow().document.getElementById(
|
||||
'codeTheme'
|
||||
).href = this.getCodeThemeLink(codeBlockTheme)
|
||||
this.getWindow().document.getElementById('style').innerHTML = buildStyle({
|
||||
).href = getCodeThemeLink(codeBlockTheme)
|
||||
|
||||
this.getWindow().document.getElementById('style').innerHTML = buildStyle(
|
||||
fontFamily,
|
||||
fontSize,
|
||||
codeBlockFontFamily,
|
||||
@@ -736,15 +425,7 @@ 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() {
|
||||
@@ -778,7 +459,7 @@ class MarkdownPreview extends React.Component {
|
||||
this.refs.root.contentWindow.document.body.setAttribute('data-theme', theme)
|
||||
if (sanitize === 'NONE') {
|
||||
const splitWithCodeTag = value.split('```')
|
||||
value = this.escapeHtmlCharactersInCodeTag(splitWithCodeTag)
|
||||
value = escapeHtmlCharactersInCodeTag(splitWithCodeTag)
|
||||
}
|
||||
const renderedHTML = this.markdown.render(value)
|
||||
attachmentManagement.migrateAttachments(value, storagePath, noteKey)
|
||||
@@ -841,13 +522,9 @@ 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 => {
|
||||
@@ -1070,17 +747,18 @@ class MarkdownPreview extends React.Component {
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @param {Number} targetRow
|
||||
* @param {Number} targetLine
|
||||
*/
|
||||
scrollToRow(targetRow) {
|
||||
scrollToLine(targetLine) {
|
||||
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 row = parseInt(block.getAttribute('data-line'))
|
||||
if (row > targetRow || index === blocks.length - 1) {
|
||||
const line = parseInt(block.getAttribute('data-line'))
|
||||
|
||||
if (line > targetLine || index === blocks.length - 1) {
|
||||
block = blocks[index - 1]
|
||||
block != null && this.scrollTo(0, block.offsetTop)
|
||||
break
|
||||
@@ -1117,7 +795,10 @@ class MarkdownPreview extends React.Component {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
const rawHref = e.target.getAttribute('href')
|
||||
const el = e.target.closest('a[href]')
|
||||
if (!el) return
|
||||
|
||||
const rawHref = el.getAttribute('href')
|
||||
const { dispatch } = this.props
|
||||
if (!rawHref) return // not checked href because parser will create file://... string for [empty link]()
|
||||
|
||||
@@ -1126,8 +807,6 @@ class MarkdownPreview extends React.Component {
|
||||
const isStartWithHash = rawHref[0] === '#'
|
||||
const { href, hash } = parser
|
||||
|
||||
if (!rawHref) return // not checked href because parser will create file://... string for [empty link]()
|
||||
|
||||
const linkHash = hash === '' ? rawHref : hash // needed because we're having special link formats that are removed by parser e.g. :line:10
|
||||
|
||||
const extractIdRegex = /file:\/\/.*main.?\w*.html#/ // file://path/to/main(.development.)html
|
||||
|
||||
@@ -13,10 +13,77 @@ class MarkdownSplitEditor extends React.Component {
|
||||
this.value = props.value
|
||||
this.focus = () => this.refs.code.focus()
|
||||
this.reload = () => this.refs.code.reload()
|
||||
this.userScroll = true
|
||||
this.userScroll = props.config.preview.scrollSync
|
||||
this.state = {
|
||||
isSliderFocused: false,
|
||||
codeEditorWidthInPercent: 50
|
||||
codeEditorWidthInPercent: 50,
|
||||
codeEditorHeightInPercent: 50
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,59 +96,125 @@ class MarkdownSplitEditor extends React.Component {
|
||||
this.props.onChange(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
|
||||
|
||||
handleEditorScroll(e) {
|
||||
if (this.userScroll) {
|
||||
if (e.doc) {
|
||||
srcTop = _.get(e, 'doc.scrollTop')
|
||||
srcHeight = _.get(e, 'doc.height')
|
||||
targetTop = _.get(previewDoc, 'body.scrollTop')
|
||||
targetHeight = _.get(previewDoc, 'body.scrollHeight')
|
||||
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')
|
||||
} else {
|
||||
srcTop = _.get(previewDoc, 'body.scrollTop')
|
||||
srcHeight = _.get(previewDoc, 'body.scrollHeight')
|
||||
targetTop = _.get(codeDoc, 'scrollTop')
|
||||
targetHeight = _.get(codeDoc, 'height')
|
||||
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)
|
||||
}
|
||||
|
||||
const distance = (targetHeight * srcTop) / srcHeight - targetTop
|
||||
const framerate = 1000 / 60
|
||||
const frames = 20
|
||||
const refractory = frames * framerate
|
||||
this.scrollTo(previewTop, top, y =>
|
||||
_.set(previewDoc, 'body.scrollTop', y)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
this.userScroll = false
|
||||
handlePreviewScroll(e) {
|
||||
if (this.userScroll) {
|
||||
const previewDoc = _.get(
|
||||
this,
|
||||
'refs.preview.refs.root.contentWindow.document'
|
||||
)
|
||||
const codeDoc = _.get(this, 'refs.code.editor.doc')
|
||||
|
||||
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)
|
||||
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
|
||||
}
|
||||
}
|
||||
frame++
|
||||
}, framerate)
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,22 +247,42 @@ class MarkdownSplitEditor extends React.Component {
|
||||
handleMouseMove(e) {
|
||||
if (this.state.isSliderFocused) {
|
||||
const rootRect = this.refs.root.getBoundingClientRect()
|
||||
const rootWidth = rootRect.width
|
||||
const offset = rootRect.left
|
||||
let newCodeEditorWidthInPercent = ((e.pageX - offset) / rootWidth) * 100
|
||||
if (this.props.isStacking) {
|
||||
const rootHeight = rootRect.height
|
||||
const offset = rootRect.top
|
||||
let newCodeEditorHeightInPercent =
|
||||
((e.pageY - offset) / rootHeight) * 100
|
||||
|
||||
// limit minSize to 10%, maxSize to 90%
|
||||
if (newCodeEditorWidthInPercent <= 10) {
|
||||
newCodeEditorWidthInPercent = 10
|
||||
// limit minSize to 10%, maxSize to 90%
|
||||
if (newCodeEditorHeightInPercent <= 10) {
|
||||
newCodeEditorHeightInPercent = 10
|
||||
}
|
||||
|
||||
if (newCodeEditorHeightInPercent >= 90) {
|
||||
newCodeEditorHeightInPercent = 90
|
||||
}
|
||||
|
||||
this.setState({
|
||||
codeEditorHeightInPercent: newCodeEditorHeightInPercent
|
||||
})
|
||||
} else {
|
||||
const rootWidth = rootRect.width
|
||||
const offset = rootRect.left
|
||||
let newCodeEditorWidthInPercent = ((e.pageX - offset) / rootWidth) * 100
|
||||
|
||||
// limit minSize to 10%, maxSize to 90%
|
||||
if (newCodeEditorWidthInPercent <= 10) {
|
||||
newCodeEditorWidthInPercent = 10
|
||||
}
|
||||
|
||||
if (newCodeEditorWidthInPercent >= 90) {
|
||||
newCodeEditorWidthInPercent = 90
|
||||
}
|
||||
|
||||
this.setState({
|
||||
codeEditorWidthInPercent: newCodeEditorWidthInPercent
|
||||
})
|
||||
}
|
||||
|
||||
if (newCodeEditorWidthInPercent >= 90) {
|
||||
newCodeEditorWidthInPercent = 90
|
||||
}
|
||||
|
||||
this.setState({
|
||||
codeEditorWidthInPercent: newCodeEditorWidthInPercent
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,6 +300,35 @@ 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,
|
||||
@@ -154,17 +336,72 @@ class MarkdownSplitEditor extends React.Component {
|
||||
storageKey,
|
||||
noteKey,
|
||||
linesHighlighted,
|
||||
getNote,
|
||||
isStacking,
|
||||
RTL
|
||||
} = this.props
|
||||
const storage = findStorage(storageKey)
|
||||
let storage
|
||||
try {
|
||||
storage = findStorage(storageKey)
|
||||
} catch (e) {
|
||||
return <div />
|
||||
}
|
||||
|
||||
let editorStyle = {}
|
||||
let previewStyle = {}
|
||||
let sliderStyle = {}
|
||||
|
||||
let editorFontSize = parseInt(config.editor.fontSize, 10)
|
||||
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
|
||||
editorStyle.fontSize = editorFontSize
|
||||
|
||||
let editorIndentSize = parseInt(config.editor.indentSize, 10)
|
||||
if (!(editorFontSize > 0 && editorFontSize < 132)) editorIndentSize = 4
|
||||
const previewStyle = {}
|
||||
previewStyle.width = 100 - this.state.codeEditorWidthInPercent + '%'
|
||||
if (!(editorStyle.fontSize > 0 && editorStyle.fontSize < 132))
|
||||
editorIndentSize = 4
|
||||
editorStyle.indentSize = editorIndentSize
|
||||
|
||||
editorStyle = Object.assign(
|
||||
editorStyle,
|
||||
isStacking
|
||||
? {
|
||||
width: '100%',
|
||||
height: `${this.state.codeEditorHeightInPercent}%`
|
||||
}
|
||||
: {
|
||||
width: `${this.state.codeEditorWidthInPercent}%`,
|
||||
height: '100%'
|
||||
}
|
||||
)
|
||||
|
||||
previewStyle = Object.assign(
|
||||
previewStyle,
|
||||
isStacking
|
||||
? {
|
||||
width: '100%',
|
||||
height: `${100 - this.state.codeEditorHeightInPercent}%`
|
||||
}
|
||||
: {
|
||||
width: `${100 - this.state.codeEditorWidthInPercent}%`,
|
||||
height: '100%'
|
||||
}
|
||||
)
|
||||
|
||||
sliderStyle = Object.assign(
|
||||
sliderStyle,
|
||||
isStacking
|
||||
? {
|
||||
left: 0,
|
||||
top: `${this.state.codeEditorHeightInPercent}%`
|
||||
}
|
||||
: {
|
||||
left: `${this.state.codeEditorWidthInPercent}%`,
|
||||
top: 0
|
||||
}
|
||||
)
|
||||
|
||||
if (this.props.ignorePreviewPointerEvents || this.state.isSliderFocused)
|
||||
previewStyle.pointerEvents = 'none'
|
||||
|
||||
return (
|
||||
<div
|
||||
styleName='root'
|
||||
@@ -174,20 +411,28 @@ class MarkdownSplitEditor extends React.Component {
|
||||
>
|
||||
<CodeEditor
|
||||
ref='code'
|
||||
width={this.state.codeEditorWidthInPercent + '%'}
|
||||
width={editorStyle.width}
|
||||
height={editorStyle.height}
|
||||
mode='Boost Flavored Markdown'
|
||||
value={value}
|
||||
theme={config.editor.theme}
|
||||
keyMap={config.editor.keyMap}
|
||||
fontFamily={config.editor.fontFamily}
|
||||
fontSize={editorFontSize}
|
||||
fontSize={editorStyle.fontSize}
|
||||
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={editorIndentSize}
|
||||
indentSize={editorStyle.indentSize}
|
||||
enableRulers={config.editor.enableRulers}
|
||||
rulers={config.editor.rulers}
|
||||
scrollPastEnd={config.editor.scrollPastEnd}
|
||||
@@ -197,24 +442,27 @@ class MarkdownSplitEditor extends React.Component {
|
||||
noteKey={noteKey}
|
||||
linesHighlighted={linesHighlighted}
|
||||
onChange={e => this.handleOnChange(e)}
|
||||
onScroll={this.handleScroll.bind(this)}
|
||||
onScroll={e => this.handleEditorScroll(e)}
|
||||
onCursorActivity={e => this.handleCursorActivity(e)}
|
||||
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}
|
||||
/>
|
||||
<div
|
||||
styleName='slider'
|
||||
style={{ left: this.state.codeEditorWidthInPercent + '%' }}
|
||||
styleName={isStacking ? 'slider-hoz' : 'slider'}
|
||||
style={{ left: sliderStyle.left, top: sliderStyle.top }}
|
||||
onMouseDown={e => this.handleMouseDown(e)}
|
||||
>
|
||||
<div styleName='slider-hitbox' />
|
||||
</div>
|
||||
<MarkdownPreview
|
||||
ref='preview'
|
||||
style={previewStyle}
|
||||
theme={config.ui.theme}
|
||||
keyMap={config.editor.keyMap}
|
||||
@@ -223,6 +471,7 @@ class MarkdownSplitEditor extends React.Component {
|
||||
codeBlockTheme={config.preview.codeBlockTheme}
|
||||
codeBlockFontFamily={config.editor.fontFamily}
|
||||
lineNumber={config.preview.lineNumber}
|
||||
indentSize={editorIndentSize}
|
||||
scrollPastEnd={config.preview.scrollPastEnd}
|
||||
smartQuotes={config.preview.smartQuotes}
|
||||
smartArrows={config.preview.smartArrows}
|
||||
@@ -232,13 +481,15 @@ class MarkdownSplitEditor extends React.Component {
|
||||
tabInde='0'
|
||||
value={value}
|
||||
onCheckboxClick={e => this.handleCheckboxClick(e)}
|
||||
onScroll={this.handleScroll.bind(this)}
|
||||
onScroll={e => this.handlePreviewScroll(e)}
|
||||
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>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
height 100%
|
||||
font-size 30px
|
||||
display flex
|
||||
flex-wrap wrap
|
||||
.slider
|
||||
absolute top bottom
|
||||
top -2px
|
||||
@@ -15,6 +16,14 @@
|
||||
left -3px
|
||||
z-index 10
|
||||
cursor col-resize
|
||||
.slider-hoz
|
||||
absolute left right
|
||||
.slider-hitbox
|
||||
absolute left right
|
||||
width: 100%
|
||||
height 7px
|
||||
cursor row-resize
|
||||
|
||||
|
||||
apply-theme(theme)
|
||||
body[data-theme={theme}]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import mermaidAPI from 'mermaid'
|
||||
import mermaidAPI from 'mermaid/dist/mermaid.min.js'
|
||||
import uiThemes from 'browser/lib/ui-themes'
|
||||
|
||||
// fixes bad styling in the mermaid dark theme
|
||||
@@ -61,7 +61,6 @@ function render(element, content, theme, enableHTMLLabel) {
|
||||
|
||||
el.setAttribute('ratio', ratio)
|
||||
el.setAttribute('height', el.parentNode.clientWidth / ratio)
|
||||
console.log(el)
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
|
||||
@@ -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 { lastFindInArray } from './utils'
|
||||
import { escapeHtmlCharacters, lastFindInArray } from './utils'
|
||||
|
||||
function createGutter(str, firstLineNumber) {
|
||||
if (Number.isNaN(firstLineNumber)) firstLineNumber = 1
|
||||
@@ -31,7 +31,8 @@ class Markdown {
|
||||
html: true,
|
||||
xhtmlOut: true,
|
||||
breaks: config.preview.breaks,
|
||||
sanitize: 'STRICT'
|
||||
sanitize: 'STRICT',
|
||||
onFence: () => {}
|
||||
}
|
||||
|
||||
const updatedOptions = Object.assign(defaultOptions, options)
|
||||
@@ -266,22 +267,26 @@ 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
|
||||
@@ -298,35 +303,41 @@ 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>`
|
||||
}
|
||||
)
|
||||
|
||||
@@ -468,6 +479,16 @@ 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)
|
||||
}
|
||||
|
||||
49
browser/lib/wakatime-plugin.js
Normal file
49
browser/lib/wakatime-plugin.js
Normal file
@@ -0,0 +1,49 @@
|
||||
import config from 'browser/main/lib/ConfigManager'
|
||||
const exec = require('child_process').exec
|
||||
const path = require('path')
|
||||
let lastHeartbeat = 0
|
||||
|
||||
function sendWakatimeHeartBeat(
|
||||
storagePath,
|
||||
noteKey,
|
||||
storageName,
|
||||
{ isWrite, hasFileChanges, isFileChange }
|
||||
) {
|
||||
if (
|
||||
config.get().wakatime.isActive &&
|
||||
!!config.get().wakatime.key &&
|
||||
(new Date().getTime() - lastHeartbeat > 120000 || isFileChange)
|
||||
) {
|
||||
const notePath = path.join(storagePath, 'notes', noteKey + '.cson')
|
||||
|
||||
if (!isWrite && !hasFileChanges && !isFileChange) {
|
||||
return
|
||||
}
|
||||
|
||||
lastHeartbeat = new Date()
|
||||
const wakatimeKey = config.get().wakatime.key
|
||||
if (wakatimeKey) {
|
||||
exec(
|
||||
`wakatime --file ${notePath} --project '${storageName}' --key ${wakatimeKey} --plugin Boostnote-wakatime`,
|
||||
(error, stdOut, stdErr) => {
|
||||
if (error) {
|
||||
console.log(error)
|
||||
lastHeartbeat = 0
|
||||
} else {
|
||||
console.log(
|
||||
'wakatime',
|
||||
'isWrite',
|
||||
isWrite,
|
||||
'hasChanges',
|
||||
hasFileChanges,
|
||||
'isFileChange',
|
||||
isFileChange
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { sendWakatimeHeartBeat }
|
||||
@@ -294,7 +294,7 @@ class FolderSelect extends React.Component {
|
||||
{optionList}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
) : currentOption ? (
|
||||
<div styleName='idle' style={{ color: currentOption.folder.color }}>
|
||||
<div styleName='idle-label'>
|
||||
<i className='fa fa-folder' />
|
||||
@@ -303,7 +303,7 @@ class FolderSelect extends React.Component {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -57,9 +57,11 @@ class MarkdownNoteDetail extends React.Component {
|
||||
|
||||
this.dispatchTimer = null
|
||||
|
||||
this.toggleLockButton = this.handleToggleLockButton.bind(this)
|
||||
this.generateToc = this.handleGenerateToc.bind(this)
|
||||
this.toggleLockButton = this.handleToggleLockButton.bind(this)
|
||||
this.handleUpdateContent = this.handleUpdateContent.bind(this)
|
||||
this.handleSwitchStackDirection = this.handleSwitchStackDirection.bind(this)
|
||||
this.getNote = this.getNote.bind(this)
|
||||
}
|
||||
|
||||
focus() {
|
||||
@@ -67,6 +69,7 @@ class MarkdownNoteDetail extends React.Component {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
ee.on('editor:orientation', this.handleSwitchStackDirection)
|
||||
ee.on('topbar:togglelockbutton', this.toggleLockButton)
|
||||
ee.on('topbar:toggledirectionbutton', () => this.handleSwitchDirection())
|
||||
ee.on('topbar:togglemodebutton', () => {
|
||||
@@ -383,7 +386,7 @@ class MarkdownNoteDetail extends React.Component {
|
||||
handleSwitchMode(type) {
|
||||
// If in split mode, hide the lock button
|
||||
this.setState(
|
||||
{ editorType: type, isLockButtonShown: !(type === 'SPLIT') },
|
||||
{ editorType: type, isLockButtonShown: type !== 'SPLIT' },
|
||||
() => {
|
||||
this.focus()
|
||||
const newConfig = Object.assign({}, this.props.config)
|
||||
@@ -393,6 +396,18 @@ class MarkdownNoteDetail extends React.Component {
|
||||
)
|
||||
}
|
||||
|
||||
handleSwitchStackDirection() {
|
||||
this.setState(
|
||||
prevState => ({ isStacking: !prevState.isStacking }),
|
||||
() => {
|
||||
this.focus()
|
||||
const newConfig = Object.assign({}, this.props.config)
|
||||
newConfig.ui.isStacking = this.state.isStacking
|
||||
ConfigManager.set(newConfig)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
handleSwitchDirection() {
|
||||
if (!this.props.config.editor.rtlEnabled) {
|
||||
return
|
||||
@@ -427,9 +442,13 @@ class MarkdownNoteDetail extends React.Component {
|
||||
this.updateNote(note)
|
||||
}
|
||||
|
||||
getNote() {
|
||||
return this.state.note
|
||||
}
|
||||
|
||||
renderEditor() {
|
||||
const { config, ignorePreviewPointerEvents } = this.props
|
||||
const { note } = this.state
|
||||
const { note, isStacking } = this.state
|
||||
|
||||
if (this.state.editorType === 'EDITOR_PREVIEW') {
|
||||
return (
|
||||
@@ -442,8 +461,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}
|
||||
/>
|
||||
)
|
||||
@@ -455,9 +474,11 @@ class MarkdownNoteDetail extends React.Component {
|
||||
value={note.content}
|
||||
storageKey={note.storage}
|
||||
noteKey={note.key}
|
||||
isStacking={isStacking}
|
||||
linesHighlighted={note.linesHighlighted}
|
||||
onChange={this.handleUpdateContent}
|
||||
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
|
||||
getNote={this.getNote}
|
||||
RTL={config.editor.rtlEnabled && this.state.RTL}
|
||||
/>
|
||||
)
|
||||
@@ -479,10 +500,16 @@ class MarkdownNoteDetail extends React.Component {
|
||||
})
|
||||
})
|
||||
})
|
||||
const currentOption = options.filter(
|
||||
|
||||
const currentOption = _.find(
|
||||
options,
|
||||
option =>
|
||||
option.storage.key === storageKey && option.folder.key === folderKey
|
||||
)[0]
|
||||
)
|
||||
|
||||
// currentOption may be undefined
|
||||
const storageName = _.get(currentOption, 'storage.name') || ''
|
||||
const folderName = _.get(currentOption, 'folder.name') || ''
|
||||
|
||||
const trashTopBar = (
|
||||
<div styleName='info'>
|
||||
@@ -495,8 +522,8 @@ class MarkdownNoteDetail extends React.Component {
|
||||
/>
|
||||
<InfoButton onClick={e => this.handleInfoButtonClick(e)} />
|
||||
<InfoPanelTrashed
|
||||
storageName={currentOption.storage.name}
|
||||
folderName={currentOption.folder.name}
|
||||
storageName={storageName}
|
||||
folderName={folderName}
|
||||
updatedAt={formatDate(note.updatedAt)}
|
||||
createdAt={formatDate(note.createdAt)}
|
||||
exportAsHtml={this.exportAsHtml}
|
||||
@@ -579,8 +606,8 @@ class MarkdownNoteDetail extends React.Component {
|
||||
<InfoButton onClick={e => this.handleInfoButtonClick(e)} />
|
||||
|
||||
<InfoPanel
|
||||
storageName={currentOption.storage.name}
|
||||
folderName={currentOption.folder.name}
|
||||
storageName={storageName}
|
||||
folderName={folderName}
|
||||
noteLink={`[${note.title}](:note:${
|
||||
queryString.parse(location.search).key
|
||||
})`}
|
||||
|
||||
@@ -859,8 +859,15 @@ 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}
|
||||
@@ -870,6 +877,9 @@ class SnippetNoteDetail extends React.Component {
|
||||
enableSmartPaste={config.editor.enableSmartPaste}
|
||||
hotkey={config.hotkey}
|
||||
autoDetect={autoDetect}
|
||||
dateFormatISO8601={config.editor.dateFormatISO8601}
|
||||
storageKey={storageKey}
|
||||
noteKey={note.key}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -885,10 +895,16 @@ class SnippetNoteDetail extends React.Component {
|
||||
})
|
||||
})
|
||||
})
|
||||
const currentOption = options.filter(
|
||||
|
||||
const currentOption = _.find(
|
||||
options,
|
||||
option =>
|
||||
option.storage.key === storageKey && option.folder.key === folderKey
|
||||
)[0]
|
||||
)
|
||||
|
||||
// currentOption may be undefined
|
||||
const storageName = _.get(currentOption, 'storage.name') || ''
|
||||
const folderName = _.get(currentOption, 'folder.name') || ''
|
||||
|
||||
const trashTopBar = (
|
||||
<div styleName='info'>
|
||||
@@ -901,8 +917,8 @@ class SnippetNoteDetail extends React.Component {
|
||||
/>
|
||||
<InfoButton onClick={e => this.handleInfoButtonClick(e)} />
|
||||
<InfoPanelTrashed
|
||||
storageName={currentOption.storage.name}
|
||||
folderName={currentOption.folder.name}
|
||||
storageName={storageName}
|
||||
folderName={folderName}
|
||||
updatedAt={formatDate(note.updatedAt)}
|
||||
createdAt={formatDate(note.createdAt)}
|
||||
exportAsMd={this.showWarning}
|
||||
@@ -951,8 +967,8 @@ class SnippetNoteDetail extends React.Component {
|
||||
<InfoButton onClick={e => this.handleInfoButtonClick(e)} />
|
||||
|
||||
<InfoPanel
|
||||
storageName={currentOption.storage.name}
|
||||
folderName={currentOption.folder.name}
|
||||
storageName={storageName}
|
||||
folderName={folderName}
|
||||
noteLink={`[${note.title}](:note:${
|
||||
queryString.parse(location.search).key
|
||||
})`}
|
||||
|
||||
@@ -20,6 +20,7 @@ class TagSelect extends React.Component {
|
||||
}
|
||||
|
||||
this.handleAddTag = this.handleAddTag.bind(this)
|
||||
this.handleRenameTag = this.handleRenameTag.bind(this)
|
||||
this.onInputBlur = this.onInputBlur.bind(this)
|
||||
this.onInputChange = this.onInputChange.bind(this)
|
||||
this.onInputKeyDown = this.onInputKeyDown.bind(this)
|
||||
@@ -88,6 +89,7 @@ class TagSelect extends React.Component {
|
||||
this.buildSuggestions()
|
||||
|
||||
ee.on('editor:add-tag', this.handleAddTag)
|
||||
ee.on('sidebar:rename-tag', this.handleRenameTag)
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
@@ -96,12 +98,23 @@ class TagSelect extends React.Component {
|
||||
|
||||
componentWillUnmount() {
|
||||
ee.off('editor:add-tag', this.handleAddTag)
|
||||
ee.off('sidebar:rename-tag', this.handleRenameTag)
|
||||
}
|
||||
|
||||
handleAddTag() {
|
||||
this.refs.newTag.input.focus()
|
||||
}
|
||||
|
||||
handleRenameTag(event, tagChange) {
|
||||
const { value } = this.props
|
||||
const { tag, updatedTag } = tagChange
|
||||
const newTags = value.slice()
|
||||
|
||||
newTags[value.indexOf(tag)] = updatedTag
|
||||
this.value = newTags
|
||||
this.props.onChange()
|
||||
}
|
||||
|
||||
handleTagLabelClick(tag) {
|
||||
const { dispatch } = this.props
|
||||
|
||||
|
||||
@@ -16,8 +16,9 @@ import { store } from 'browser/main/store'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
import { getLocales } from 'browser/lib/Languages'
|
||||
import applyShortcuts from 'browser/main/lib/shortcutManager'
|
||||
import uiThemes from 'browser/lib/ui-themes'
|
||||
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')
|
||||
@@ -148,11 +149,13 @@ class Main extends React.Component {
|
||||
componentDidMount() {
|
||||
const { dispatch, config } = this.props
|
||||
|
||||
if (uiThemes.some(theme => theme.name === config.ui.theme)) {
|
||||
document.body.setAttribute('data-theme', config.ui.theme)
|
||||
} else {
|
||||
document.body.setAttribute('data-theme', 'default')
|
||||
}
|
||||
this.refreshTheme = setInterval(() => {
|
||||
const conf = ConfigManager.get()
|
||||
chooseTheme(conf)
|
||||
}, 5 * 1000)
|
||||
|
||||
chooseTheme(config)
|
||||
applyTheme(config.ui.theme)
|
||||
|
||||
if (getLocales().indexOf(config.ui.language) !== -1) {
|
||||
i18n.setLocale(config.ui.language)
|
||||
@@ -181,6 +184,8 @@ class Main extends React.Component {
|
||||
'menubar:togglemenubar',
|
||||
this.toggleMenuBarVisible.bind(this)
|
||||
)
|
||||
eventEmitter.on('dispatch:push', this.changeRoutePush.bind(this))
|
||||
eventEmitter.on('update', () => ipcRenderer.send('update-check', 'manual'))
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
@@ -189,6 +194,13 @@ class Main extends React.Component {
|
||||
'menubar:togglemenubar',
|
||||
this.toggleMenuBarVisible.bind(this)
|
||||
)
|
||||
eventEmitter.off('dispatch:push', this.changeRoutePush.bind(this))
|
||||
clearInterval(this.refreshTheme)
|
||||
}
|
||||
|
||||
changeRoutePush(event, destination) {
|
||||
const { dispatch } = this.props
|
||||
dispatch(push(destination))
|
||||
}
|
||||
|
||||
toggleMenuBarVisible() {
|
||||
|
||||
@@ -21,6 +21,7 @@ import Markdown from '../../lib/markdown'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
|
||||
import context from 'browser/lib/context'
|
||||
import filenamify from 'filenamify'
|
||||
import queryString from 'query-string'
|
||||
|
||||
const { remote } = require('electron')
|
||||
@@ -634,6 +635,38 @@ class NoteList extends React.Component {
|
||||
this.selectNextNote()
|
||||
}
|
||||
|
||||
handleExportClick(e, note, fileType) {
|
||||
const options = {
|
||||
defaultPath: filenamify(note.title, {
|
||||
replacement: '_'
|
||||
}),
|
||||
filters: [{ name: 'Documents', extensions: [fileType] }],
|
||||
properties: ['openFile', 'createDirectory']
|
||||
}
|
||||
|
||||
dialog.showSaveDialog(remote.getCurrentWindow(), options, filename => {
|
||||
if (filename) {
|
||||
const { config } = this.props
|
||||
|
||||
dataApi
|
||||
.exportNoteAs(note, filename, fileType, config)
|
||||
.then(res => {
|
||||
dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
type: 'info',
|
||||
message: `Exported to ${filename}`
|
||||
})
|
||||
})
|
||||
.catch(err => {
|
||||
dialog.showErrorBox(
|
||||
'Export error',
|
||||
err ? err.message || err : 'Unexpected error during export'
|
||||
)
|
||||
throw err
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
handleNoteContextMenu(e, uniqueKey) {
|
||||
const { location } = this.props
|
||||
const { selectedNoteKeys } = this.state
|
||||
@@ -689,9 +722,40 @@ 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)
|
||||
@@ -702,10 +766,15 @@ class NoteList extends React.Component {
|
||||
}
|
||||
)
|
||||
} else {
|
||||
templates.push({
|
||||
label: publishLabel,
|
||||
click: this.publishMarkdown.bind(this)
|
||||
})
|
||||
templates.push(
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: publishLabel,
|
||||
click: this.publishMarkdown.bind(this)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1107,10 +1176,10 @@ class NoteList extends React.Component {
|
||||
|
||||
getNoteFolder(note) {
|
||||
// note.folder = folder key
|
||||
return _.find(
|
||||
this.getNoteStorage(note).folders,
|
||||
({ key }) => key === note.folder
|
||||
)
|
||||
const storage = this.getNoteStorage(note)
|
||||
return storage
|
||||
? _.find(storage.folders, ({ key }) => key === note.folder)
|
||||
: []
|
||||
}
|
||||
|
||||
getViewType() {
|
||||
@@ -1145,9 +1214,14 @@ class NoteList extends React.Component {
|
||||
? this.getNotes().sort(sortFunc)
|
||||
: this.sortByPin(this.getNotes().sort(sortFunc))
|
||||
this.notes = notes = sortedNotes.filter(note => {
|
||||
// this is for the trash box
|
||||
if (note.isTrashed !== true || location.pathname === '/trashed')
|
||||
if (
|
||||
// has matching storage
|
||||
!!this.getNoteStorage(note) &&
|
||||
// this is for the trash box
|
||||
(note.isTrashed !== true || location.pathname === '/trashed')
|
||||
) {
|
||||
return true
|
||||
}
|
||||
})
|
||||
if (sortDir === 'DESCENDING') this.notes.reverse()
|
||||
|
||||
@@ -1193,6 +1267,8 @@ class NoteList extends React.Component {
|
||||
sortBy === 'CREATED_AT' ? note.createdAt : note.updatedAt
|
||||
).fromNow('D')
|
||||
|
||||
const storage = this.getNoteStorage(note)
|
||||
|
||||
if (isDefault) {
|
||||
return (
|
||||
<NoteItem
|
||||
@@ -1205,7 +1281,7 @@ class NoteList extends React.Component {
|
||||
handleDragStart={this.handleDragStart.bind(this)}
|
||||
pathname={location.pathname}
|
||||
folderName={this.getNoteFolder(note).name}
|
||||
storageName={this.getNoteStorage(note).name}
|
||||
storageName={storage.name}
|
||||
viewType={viewType}
|
||||
showTagsAlphabetically={config.ui.showTagsAlphabetically}
|
||||
coloredTags={config.coloredTags}
|
||||
@@ -1223,7 +1299,7 @@ class NoteList extends React.Component {
|
||||
handleDragStart={this.handleDragStart.bind(this)}
|
||||
pathname={location.pathname}
|
||||
folderName={this.getNoteFolder(note).name}
|
||||
storageName={this.getNoteStorage(note).name}
|
||||
storageName={storage.name}
|
||||
viewType={viewType}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -43,12 +43,20 @@ class StorageItem extends React.Component {
|
||||
label: i18n.__('Export Storage'),
|
||||
submenu: [
|
||||
{
|
||||
label: i18n.__('Export as txt'),
|
||||
label: i18n.__('Export as Plain Text (.txt)'),
|
||||
click: e => this.handleExportStorageClick(e, 'txt')
|
||||
},
|
||||
{
|
||||
label: i18n.__('Export as md'),
|
||||
label: i18n.__('Export as Markdown (.md)'),
|
||||
click: e => this.handleExportStorageClick(e, 'md')
|
||||
},
|
||||
{
|
||||
label: i18n.__('Export as HTML (.html)'),
|
||||
click: e => this.handleExportStorageClick(e, 'html')
|
||||
},
|
||||
{
|
||||
label: i18n.__('Export as PDF (.pdf)'),
|
||||
click: e => this.handleExportStorageClick(e, 'pdf')
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -97,14 +105,28 @@ class StorageItem extends React.Component {
|
||||
}
|
||||
dialog.showOpenDialog(remote.getCurrentWindow(), options, paths => {
|
||||
if (paths && paths.length === 1) {
|
||||
const { storage, dispatch } = this.props
|
||||
dataApi.exportStorage(storage.key, fileType, paths[0]).then(data => {
|
||||
dispatch({
|
||||
type: 'EXPORT_STORAGE',
|
||||
storage: data.storage,
|
||||
fileType: data.fileType
|
||||
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
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -166,12 +188,20 @@ class StorageItem extends React.Component {
|
||||
label: i18n.__('Export Folder'),
|
||||
submenu: [
|
||||
{
|
||||
label: i18n.__('Export as txt'),
|
||||
label: i18n.__('Export as Plain Text (.txt)'),
|
||||
click: e => this.handleExportFolderClick(e, folder, 'txt')
|
||||
},
|
||||
{
|
||||
label: i18n.__('Export as md'),
|
||||
label: i18n.__('Export as Markdown (.md)'),
|
||||
click: e => this.handleExportFolderClick(e, folder, 'md')
|
||||
},
|
||||
{
|
||||
label: i18n.__('Export as HTML (.html)'),
|
||||
click: e => this.handleExportFolderClick(e, folder, 'html')
|
||||
},
|
||||
{
|
||||
label: i18n.__('Export as PDF (.pdf)'),
|
||||
click: e => this.handleExportFolderClick(e, folder, 'pdf')
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -202,30 +232,28 @@ class StorageItem extends React.Component {
|
||||
}
|
||||
dialog.showOpenDialog(remote.getCurrentWindow(), options, paths => {
|
||||
if (paths && paths.length === 1) {
|
||||
const { storage, dispatch } = this.props
|
||||
const { storage, dispatch, config } = this.props
|
||||
dataApi
|
||||
.exportFolder(storage.key, folder.key, fileType, paths[0])
|
||||
.exportFolder(storage.key, folder.key, fileType, paths[0], config)
|
||||
.then(data => {
|
||||
dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
type: 'info',
|
||||
message: `Exported to ${paths[0]}`
|
||||
})
|
||||
|
||||
dispatch({
|
||||
type: 'EXPORT_FOLDER',
|
||||
storage: data.storage,
|
||||
folderKey: data.folderKey,
|
||||
fileType: data.fileType
|
||||
})
|
||||
return data
|
||||
})
|
||||
.then(data => {
|
||||
dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
type: 'info',
|
||||
message: 'Exported to "' + data.exportDir + '"'
|
||||
})
|
||||
})
|
||||
.catch(err => {
|
||||
.catch(error => {
|
||||
dialog.showErrorBox(
|
||||
'Export error',
|
||||
err ? err.message || err : 'Unexpected error during export'
|
||||
error ? error.message || error : 'Unexpected error during export'
|
||||
)
|
||||
throw err
|
||||
throw error
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -6,6 +6,7 @@ import dataApi from 'browser/main/lib/dataApi'
|
||||
import styles from './SideNav.styl'
|
||||
import { openModal } from 'browser/main/lib/modal'
|
||||
import PreferencesModal from '../modals/PreferencesModal'
|
||||
import RenameTagModal from 'browser/main/modals/RenameTagModal'
|
||||
import ConfigManager from 'browser/main/lib/ConfigManager'
|
||||
import StorageItem from './StorageItem'
|
||||
import TagListItem from 'browser/components/TagListItem'
|
||||
@@ -25,6 +26,8 @@ import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
|
||||
import ColorPicker from 'browser/components/ColorPicker'
|
||||
import { every, sortBy } from 'lodash'
|
||||
|
||||
const { dialog } = remote
|
||||
|
||||
function matchActiveTags(tags, activeTags) {
|
||||
return every(activeTags, v => tags.indexOf(v) >= 0)
|
||||
}
|
||||
@@ -62,15 +65,12 @@ class SideNav extends React.Component {
|
||||
}
|
||||
|
||||
deleteTag(tag) {
|
||||
const selectedButton = remote.dialog.showMessageBox(
|
||||
remote.getCurrentWindow(),
|
||||
{
|
||||
type: 'warning',
|
||||
message: i18n.__('Confirm tag deletion'),
|
||||
detail: i18n.__('This will permanently remove this tag.'),
|
||||
buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
|
||||
}
|
||||
)
|
||||
const selectedButton = dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
type: 'warning',
|
||||
message: i18n.__('Confirm tag deletion'),
|
||||
detail: i18n.__('This will permanently remove this tag.'),
|
||||
buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
|
||||
})
|
||||
|
||||
if (selectedButton === 0) {
|
||||
const {
|
||||
@@ -154,23 +154,80 @@ class SideNav extends React.Component {
|
||||
}
|
||||
|
||||
handleTagContextMenu(e, tag) {
|
||||
const menu = []
|
||||
context.popup([
|
||||
{
|
||||
label: i18n.__('Rename Tag'),
|
||||
click: this.handleRenameTagClick.bind(this, tag)
|
||||
},
|
||||
{
|
||||
label: i18n.__('Customize Color'),
|
||||
click: this.displayColorPicker.bind(
|
||||
this,
|
||||
tag,
|
||||
e.target.getBoundingClientRect()
|
||||
)
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: i18n.__('Export Tag'),
|
||||
submenu: [
|
||||
{
|
||||
label: i18n.__('Export as Plain Text (.txt)'),
|
||||
click: e => this.handleExportTagClick(e, tag, 'txt')
|
||||
},
|
||||
{
|
||||
label: i18n.__('Export as Markdown (.md)'),
|
||||
click: e => this.handleExportTagClick(e, tag, 'md')
|
||||
},
|
||||
{
|
||||
label: i18n.__('Export as HTML (.html)'),
|
||||
click: e => this.handleExportTagClick(e, tag, 'html')
|
||||
},
|
||||
{
|
||||
label: i18n.__('Export as PDF (.pdf)'),
|
||||
click: e => this.handleExportTagClick(e, tag, 'pdf')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: i18n.__('Delete Tag'),
|
||||
click: this.deleteTag.bind(this, tag)
|
||||
}
|
||||
])
|
||||
}
|
||||
|
||||
menu.push({
|
||||
label: i18n.__('Delete Tag'),
|
||||
click: this.deleteTag.bind(this, tag)
|
||||
handleExportTagClick(e, tag, fileType) {
|
||||
const options = {
|
||||
properties: ['openDirectory', 'createDirectory'],
|
||||
buttonLabel: i18n.__('Select directory'),
|
||||
title: i18n.__('Select a folder to export the files to'),
|
||||
multiSelections: false
|
||||
}
|
||||
dialog.showOpenDialog(remote.getCurrentWindow(), options, paths => {
|
||||
if (paths && paths.length === 1) {
|
||||
const { data, config } = this.props
|
||||
dataApi
|
||||
.exportTag(data, tag, fileType, paths[0], config)
|
||||
.then(data => {
|
||||
dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
type: 'info',
|
||||
message: `Exported to ${paths[0]}`
|
||||
})
|
||||
})
|
||||
.catch(error => {
|
||||
dialog.showErrorBox(
|
||||
'Export error',
|
||||
error ? error.message || error : 'Unexpected error during export'
|
||||
)
|
||||
throw error
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
menu.push({
|
||||
label: i18n.__('Customize Color'),
|
||||
click: this.displayColorPicker.bind(
|
||||
this,
|
||||
tag,
|
||||
e.target.getBoundingClientRect()
|
||||
)
|
||||
})
|
||||
|
||||
context.popup(menu)
|
||||
}
|
||||
|
||||
dismissColorPicker() {
|
||||
@@ -193,6 +250,16 @@ class SideNav extends React.Component {
|
||||
})
|
||||
}
|
||||
|
||||
handleRenameTagClick(tagName) {
|
||||
const { data, dispatch } = this.props
|
||||
|
||||
openModal(RenameTagModal, {
|
||||
tagName,
|
||||
data,
|
||||
dispatch
|
||||
})
|
||||
}
|
||||
|
||||
handleColorPickerConfirm(color) {
|
||||
const {
|
||||
dispatch,
|
||||
@@ -314,6 +381,7 @@ class SideNav extends React.Component {
|
||||
dispatch={dispatch}
|
||||
onSortEnd={this.onSortEnd.bind(this)(storage)}
|
||||
useDragHandle
|
||||
config={config}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -12,6 +12,7 @@ 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')
|
||||
|
||||
@@ -107,6 +108,22 @@ 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}>
|
||||
@@ -147,8 +164,12 @@ ReactDOM.render(
|
||||
})
|
||||
|
||||
ipcRenderer.on('update-found', function() {
|
||||
notify('Update found!', {
|
||||
body: 'Preparing to update...'
|
||||
downloadUpdate()
|
||||
})
|
||||
|
||||
ipcRenderer.on('update-not-found', function(_, msg) {
|
||||
notify('Update not found!', {
|
||||
body: msg
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import _ from 'lodash'
|
||||
import RcParser from 'browser/lib/RcParser'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
import ee from 'browser/main/lib/eventEmitter'
|
||||
import uiThemes from 'browser/lib/ui-themes'
|
||||
|
||||
const OSX = global.process.platform === 'darwin'
|
||||
const win = global.process.platform === 'win32'
|
||||
@@ -17,6 +16,22 @@ const DEFAULT_MARKDOWN_LINT_CONFIG = `{
|
||||
"default": true
|
||||
}`
|
||||
|
||||
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
|
||||
{
|
||||
padding: 2px;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-radius: 5px;
|
||||
background-color: #F4F4F4;
|
||||
border-color: #d9d9d9;
|
||||
color: #03C588;
|
||||
}
|
||||
`
|
||||
|
||||
export const DEFAULT_CONFIG = {
|
||||
zoom: 1,
|
||||
isSideNavFolded: false,
|
||||
@@ -47,11 +62,17 @@ export const DEFAULT_CONFIG = {
|
||||
ui: {
|
||||
language: 'en',
|
||||
theme: 'default',
|
||||
defaultTheme: 'default',
|
||||
enableScheduleTheme: false,
|
||||
scheduledTheme: 'monokai',
|
||||
scheduleStart: 1200,
|
||||
scheduleEnd: 360,
|
||||
showCopyNotification: true,
|
||||
disableDirectWrite: false,
|
||||
showScrollBar: true,
|
||||
defaultNote: 'ALWAYS_ASK', // 'ALWAYS_ASK', 'SNIPPET_NOTE', 'MARKDOWN_NOTE'
|
||||
showMenuBar: false
|
||||
showMenuBar: false,
|
||||
isStacking: false
|
||||
},
|
||||
editor: {
|
||||
theme: 'base16-light',
|
||||
@@ -65,8 +86,13 @@ 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,
|
||||
@@ -79,7 +105,8 @@ export const DEFAULT_CONFIG = {
|
||||
enableSmartPaste: false,
|
||||
enableMarkdownLint: false,
|
||||
customMarkdownLintConfig: DEFAULT_MARKDOWN_LINT_CONFIG,
|
||||
prettierConfig: ` {
|
||||
dateFormatISO8601: false,
|
||||
prettierConfig: `{
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 2,
|
||||
"semi": false,
|
||||
@@ -104,8 +131,7 @@ export const DEFAULT_CONFIG = {
|
||||
breaks: true,
|
||||
smartArrows: false,
|
||||
allowCustomCSS: false,
|
||||
|
||||
customCSS: '/* Drop Your Custom CSS Code Here */',
|
||||
customCSS: DEFAULT_CSS_CONFIG,
|
||||
sanitize: 'STRICT', // 'STRICT', 'ALLOW_STYLES', 'NONE'
|
||||
mermaidHTMLLabel: false,
|
||||
lineThroughCheckbox: true
|
||||
@@ -118,7 +144,15 @@ export const DEFAULT_CONFIG = {
|
||||
username: '',
|
||||
password: ''
|
||||
},
|
||||
coloredTags: {}
|
||||
export: {
|
||||
metadata: 'DONT_EXPORT', // 'DONT_EXPORT', 'MERGE_HEADER', 'MERGE_VARIABLE'
|
||||
variable: 'boostnote',
|
||||
prefixAttachmentFolder: false
|
||||
},
|
||||
coloredTags: {},
|
||||
wakatime: {
|
||||
key: null
|
||||
}
|
||||
}
|
||||
|
||||
function validate(config) {
|
||||
@@ -200,12 +234,6 @@ function set(updates) {
|
||||
if (!validate(newConfig)) throw new Error('INVALID CONFIG')
|
||||
_save(newConfig)
|
||||
|
||||
if (uiThemes.some(theme => theme.name === newConfig.ui.theme)) {
|
||||
document.body.setAttribute('data-theme', newConfig.ui.theme)
|
||||
} else {
|
||||
document.body.setAttribute('data-theme', 'default')
|
||||
}
|
||||
|
||||
i18n.setLocale(newConfig.ui.language)
|
||||
|
||||
let editorTheme = document.getElementById('editorTheme')
|
||||
@@ -240,6 +268,12 @@ function assignConfigValues(originalConfig, rcConfig) {
|
||||
originalConfig.hotkey,
|
||||
rcConfig.hotkey
|
||||
)
|
||||
config.wakatime = Object.assign(
|
||||
{},
|
||||
DEFAULT_CONFIG.wakatime,
|
||||
originalConfig.wakatime,
|
||||
rcConfig.wakatime
|
||||
)
|
||||
config.blog = Object.assign(
|
||||
{},
|
||||
DEFAULT_CONFIG.blog,
|
||||
|
||||
59
browser/main/lib/ThemeManager.js
Normal file
59
browser/main/lib/ThemeManager.js
Normal file
@@ -0,0 +1,59 @@
|
||||
import ConfigManager from 'browser/main/lib/ConfigManager'
|
||||
import uiThemes from 'browser/lib/ui-themes'
|
||||
|
||||
const saveChanges = newConfig => {
|
||||
ConfigManager.set(newConfig)
|
||||
}
|
||||
|
||||
const chooseTheme = config => {
|
||||
const { ui } = config
|
||||
if (!ui.enableScheduleTheme) {
|
||||
return
|
||||
}
|
||||
|
||||
const start = parseInt(ui.scheduleStart)
|
||||
const end = parseInt(ui.scheduleEnd)
|
||||
|
||||
const now = new Date()
|
||||
const minutes = now.getHours() * 60 + now.getMinutes()
|
||||
|
||||
const isEndAfterStart = end > start
|
||||
const isBetweenStartAndEnd = minutes >= start && minutes < end
|
||||
const isBetweenEndAndStart = minutes >= start || minutes < end
|
||||
|
||||
if (
|
||||
(isEndAfterStart && isBetweenStartAndEnd) ||
|
||||
(!isEndAfterStart && isBetweenEndAndStart)
|
||||
) {
|
||||
if (ui.theme !== ui.scheduledTheme) {
|
||||
ui.defaultTheme = ui.theme
|
||||
ui.theme = ui.scheduledTheme
|
||||
applyTheme(ui.theme)
|
||||
saveChanges(config)
|
||||
}
|
||||
} else {
|
||||
if (ui.theme !== ui.defaultTheme) {
|
||||
ui.theme = ui.defaultTheme
|
||||
applyTheme(ui.theme)
|
||||
saveChanges(config)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const applyTheme = theme => {
|
||||
if (uiThemes.some(item => item.name === theme)) {
|
||||
document.body.setAttribute('data-theme', theme)
|
||||
if (document.body.querySelector('.MarkdownPreview')) {
|
||||
document.body
|
||||
.querySelector('.MarkdownPreview')
|
||||
.contentDocument.body.setAttribute('data-theme', theme)
|
||||
}
|
||||
} else {
|
||||
document.body.setAttribute('data-theme', 'default')
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
chooseTheme,
|
||||
applyTheme
|
||||
}
|
||||
@@ -706,31 +706,38 @@ function replaceNoteKeyWithNewNoteKey(noteContent, oldNoteKey, newNoteKey) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Deletes all :storage and noteKey references from the given input.
|
||||
* @param input Input in which the references should be deleted
|
||||
* @description replace all :storage references with given destination folder.
|
||||
* @param input Input in which the references should be replaced
|
||||
* @param noteKey Key of the current note
|
||||
* @param destinationFolder Destination folder of the attachements
|
||||
* @returns {String} Input without the references
|
||||
*/
|
||||
function removeStorageAndNoteReferences(input, noteKey) {
|
||||
function replaceStorageReferences(input, noteKey, destinationFolder) {
|
||||
return input.replace(
|
||||
new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER + '.*?("|])', 'g'),
|
||||
new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER + '[^"\\)<\\s]+', 'g'),
|
||||
function(match) {
|
||||
const temp = match
|
||||
.replace(new RegExp(mdurl.encode(path.win32.sep), 'g'), path.sep)
|
||||
.replace(new RegExp(mdurl.encode(path.posix.sep), 'g'), path.sep)
|
||||
.replace(new RegExp(escapeStringRegexp(path.win32.sep), 'g'), path.sep)
|
||||
.replace(new RegExp(escapeStringRegexp(path.posix.sep), 'g'), path.sep)
|
||||
return temp.replace(
|
||||
new RegExp(
|
||||
STORAGE_FOLDER_PLACEHOLDER +
|
||||
'(' +
|
||||
escapeStringRegexp(path.sep) +
|
||||
noteKey +
|
||||
')?',
|
||||
'g'
|
||||
),
|
||||
DESTINATION_FOLDER
|
||||
)
|
||||
return match
|
||||
.replace(new RegExp(mdurl.encode(path.win32.sep), 'g'), path.posix.sep)
|
||||
.replace(new RegExp(mdurl.encode(path.posix.sep), 'g'), path.posix.sep)
|
||||
.replace(
|
||||
new RegExp(escapeStringRegexp(path.win32.sep), 'g'),
|
||||
path.posix.sep
|
||||
)
|
||||
.replace(
|
||||
new RegExp(escapeStringRegexp(path.posix.sep), 'g'),
|
||||
path.posix.sep
|
||||
)
|
||||
.replace(
|
||||
new RegExp(
|
||||
STORAGE_FOLDER_PLACEHOLDER +
|
||||
'(' +
|
||||
escapeStringRegexp(path.sep) +
|
||||
noteKey +
|
||||
')?',
|
||||
'g'
|
||||
),
|
||||
destinationFolder
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -835,7 +842,15 @@ function getAttachmentsPathAndStatus(markdownContent, storageKey, noteKey) {
|
||||
if (storageKey == null || noteKey == null || markdownContent == null) {
|
||||
return null
|
||||
}
|
||||
const targetStorage = findStorage.findStorage(storageKey)
|
||||
let targetStorage = null
|
||||
try {
|
||||
targetStorage = findStorage.findStorage(storageKey)
|
||||
} catch (error) {
|
||||
console.warn(`No stroage found for: ${storageKey}`)
|
||||
}
|
||||
if (!targetStorage) {
|
||||
return null
|
||||
}
|
||||
const attachmentFolder = path.join(
|
||||
targetStorage.path,
|
||||
DESTINATION_FOLDER,
|
||||
@@ -1087,8 +1102,8 @@ module.exports = {
|
||||
getAttachmentsInMarkdownContent,
|
||||
getAbsolutePathsOfAttachmentsInContent,
|
||||
importAttachments,
|
||||
removeStorageAndNoteReferences,
|
||||
removeAttachmentsByPaths,
|
||||
replaceStorageReferences,
|
||||
deleteAttachmentFolder,
|
||||
deleteAttachmentsNotPresentInNote,
|
||||
getAttachmentsPathAndStatus,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
import fs from 'fs'
|
||||
import fx from 'fs-extra'
|
||||
import path from 'path'
|
||||
|
||||
/**
|
||||
* @description Copy a file from source to destination
|
||||
@@ -14,7 +15,8 @@ function copyFile(srcPath, dstPath) {
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const dstFolder = path.dirname(dstPath)
|
||||
if (!fs.existsSync(dstFolder)) fs.mkdirSync(dstFolder)
|
||||
|
||||
fx.ensureDirSync(dstFolder)
|
||||
|
||||
const input = fs.createReadStream(decodeURI(srcPath))
|
||||
const output = fs.createWriteStream(dstPath)
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { findStorage } from 'browser/lib/findStorage'
|
||||
import resolveStorageData from './resolveStorageData'
|
||||
import resolveStorageNotes from './resolveStorageNotes'
|
||||
import getFilename from './getFilename'
|
||||
import exportNote from './exportNote'
|
||||
import filenamify from 'filenamify'
|
||||
import * as path from 'path'
|
||||
import getContentFormatter from './getContentFormatter'
|
||||
|
||||
/**
|
||||
* @param {String} storageKey
|
||||
* @param {String} folderKey
|
||||
* @param {String} fileType
|
||||
* @param {String} exportDir
|
||||
* @param {Object} config
|
||||
*
|
||||
* @return {Object}
|
||||
* ```
|
||||
@@ -22,7 +23,7 @@ import * as path from 'path'
|
||||
* ```
|
||||
*/
|
||||
|
||||
function exportFolder(storageKey, folderKey, fileType, exportDir) {
|
||||
function exportFolder(storageKey, folderKey, fileType, exportDir, config) {
|
||||
let targetStorage
|
||||
try {
|
||||
targetStorage = findStorage(storageKey)
|
||||
@@ -30,39 +31,34 @@ function exportFolder(storageKey, folderKey, fileType, exportDir) {
|
||||
return Promise.reject(e)
|
||||
}
|
||||
|
||||
const deduplicator = {}
|
||||
|
||||
return resolveStorageData(targetStorage)
|
||||
.then(function assignNotes(storage) {
|
||||
return resolveStorageNotes(storage).then(notes => {
|
||||
return {
|
||||
storage,
|
||||
notes
|
||||
}
|
||||
})
|
||||
.then(storage => {
|
||||
return resolveStorageNotes(storage).then(notes => ({
|
||||
storage,
|
||||
notes: notes.filter(
|
||||
note =>
|
||||
note.folder === folderKey &&
|
||||
!note.isTrashed &&
|
||||
note.type === 'MARKDOWN_NOTE'
|
||||
)
|
||||
}))
|
||||
})
|
||||
.then(function exportNotes(data) {
|
||||
const { storage, notes } = data
|
||||
.then(({ storage, notes }) => {
|
||||
const contentFormatter = getContentFormatter(storage, fileType, config)
|
||||
|
||||
return Promise.all(
|
||||
notes
|
||||
.filter(
|
||||
note =>
|
||||
note.folder === folderKey &&
|
||||
note.isTrashed === false &&
|
||||
note.type === 'MARKDOWN_NOTE'
|
||||
notes.map(note => {
|
||||
const targetPath = getFilename(
|
||||
note,
|
||||
fileType,
|
||||
exportDir,
|
||||
deduplicator
|
||||
)
|
||||
.map(note => {
|
||||
const notePath = path.join(
|
||||
exportDir,
|
||||
`${filenamify(note.title, { replacement: '_' })}.${fileType}`
|
||||
)
|
||||
return exportNote(
|
||||
note.key,
|
||||
storage.path,
|
||||
note.content,
|
||||
notePath,
|
||||
null
|
||||
)
|
||||
})
|
||||
|
||||
return exportNote(storage.key, note, targetPath, contentFormatter)
|
||||
})
|
||||
).then(() => ({
|
||||
storage,
|
||||
folderKey,
|
||||
|
||||
@@ -4,58 +4,35 @@ 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 {String} noteContent Content to export
|
||||
* @param {Object} note Note to export
|
||||
* @param {String} targetPath Path to exported file
|
||||
* @param {function} outputFormatter
|
||||
* @return {Promise.<*[]>}
|
||||
*/
|
||||
function exportNote(
|
||||
nodeKey,
|
||||
storageKey,
|
||||
noteContent,
|
||||
targetPath,
|
||||
outputFormatter
|
||||
) {
|
||||
function exportNote(storageKey, note, targetPath, outputFormatter) {
|
||||
const storagePath = path.isAbsolute(storageKey)
|
||||
? storageKey
|
||||
: findStorage(storageKey).path
|
||||
|
||||
const exportTasks = []
|
||||
|
||||
if (!storagePath) {
|
||||
throw new Error('Storage path is not found')
|
||||
}
|
||||
const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent(
|
||||
noteContent,
|
||||
storagePath
|
||||
)
|
||||
attachmentsAbsolutePaths.forEach(attachment => {
|
||||
exportTasks.push({
|
||||
src: attachment,
|
||||
dst: attachmentManagement.DESTINATION_FOLDER
|
||||
})
|
||||
})
|
||||
|
||||
let exportedData = attachmentManagement.removeStorageAndNoteReferences(
|
||||
noteContent,
|
||||
nodeKey
|
||||
const exportedData = Promise.resolve(
|
||||
outputFormatter
|
||||
? outputFormatter(note, targetPath, exportTasks)
|
||||
: note.content
|
||||
)
|
||||
|
||||
if (outputFormatter) {
|
||||
exportedData = outputFormatter(exportedData, exportTasks, targetPath)
|
||||
} else {
|
||||
exportedData = Promise.resolve(exportedData)
|
||||
}
|
||||
|
||||
const tasks = prepareTasks(exportTasks, storagePath, path.dirname(targetPath))
|
||||
|
||||
return Promise.all(tasks.map(task => copyFile(task.src, task.dst)))
|
||||
@@ -63,9 +40,9 @@ function exportNote(
|
||||
.then(data => {
|
||||
return saveToFile(data, targetPath)
|
||||
})
|
||||
.catch(err => {
|
||||
.catch(error => {
|
||||
rollbackExport(tasks)
|
||||
throw err
|
||||
throw error
|
||||
})
|
||||
}
|
||||
|
||||
@@ -107,14 +84,14 @@ function rollbackExport(tasks) {
|
||||
}
|
||||
|
||||
if (fs.existsSync(fullpath)) {
|
||||
fs.unlink(fullpath)
|
||||
fs.unlinkSync(fullpath)
|
||||
folders.add(path.dirname(fullpath))
|
||||
}
|
||||
})
|
||||
|
||||
folders.forEach(folder => {
|
||||
if (fs.readdirSync(folder).length === 0) {
|
||||
fs.rmdir(folder)
|
||||
fs.rmdirSync(folder)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
19
browser/main/lib/dataApi/exportNoteAs.js
Normal file
19
browser/main/lib/dataApi/exportNoteAs.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import { findStorage } from 'browser/lib/findStorage'
|
||||
import exportNote from './exportNote'
|
||||
import getContentFormatter from './getContentFormatter'
|
||||
|
||||
/**
|
||||
* @param {Object} note
|
||||
* @param {String} filename
|
||||
* @param {String} fileType
|
||||
* @param {Object} config
|
||||
*/
|
||||
|
||||
function exportNoteAs(note, filename, fileType, config) {
|
||||
const storage = findStorage(note.storage)
|
||||
const contentFormatter = getContentFormatter(storage, fileType, config)
|
||||
|
||||
return exportNote(storage.key, note, filename, contentFormatter)
|
||||
}
|
||||
|
||||
module.exports = exportNoteAs
|
||||
@@ -2,13 +2,17 @@ import { findStorage } from 'browser/lib/findStorage'
|
||||
import resolveStorageData from './resolveStorageData'
|
||||
import resolveStorageNotes from './resolveStorageNotes'
|
||||
import filenamify from 'filenamify'
|
||||
import * as path from 'path'
|
||||
import * as fs from 'fs'
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
import exportNote from './exportNote'
|
||||
import getContentFormatter from './getContentFormatter'
|
||||
import getFilename from './getFilename'
|
||||
|
||||
/**
|
||||
* @param {String} storageKey
|
||||
* @param {String} fileType
|
||||
* @param {String} exportDir
|
||||
* @param {Object} config
|
||||
*
|
||||
* @return {Object}
|
||||
* ```
|
||||
@@ -20,7 +24,7 @@ import * as fs from 'fs'
|
||||
* ```
|
||||
*/
|
||||
|
||||
function exportStorage(storageKey, fileType, exportDir) {
|
||||
function exportStorage(storageKey, fileType, exportDir, config) {
|
||||
let targetStorage
|
||||
try {
|
||||
targetStorage = findStorage(storageKey)
|
||||
@@ -29,39 +33,52 @@ function exportStorage(storageKey, fileType, exportDir) {
|
||||
}
|
||||
|
||||
return resolveStorageData(targetStorage)
|
||||
.then(storage =>
|
||||
resolveStorageNotes(storage).then(notes => ({ storage, notes }))
|
||||
)
|
||||
.then(function exportNotes(data) {
|
||||
const { storage, notes } = data
|
||||
.then(storage => {
|
||||
return resolveStorageNotes(storage).then(notes => ({
|
||||
storage,
|
||||
notes: notes.filter(
|
||||
note => !note.isTrashed && note.type === 'MARKDOWN_NOTE'
|
||||
)
|
||||
}))
|
||||
})
|
||||
.then(({ storage, notes }) => {
|
||||
const contentFormatter = getContentFormatter(storage, fileType, config)
|
||||
|
||||
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) {}
|
||||
})
|
||||
notes
|
||||
.filter(note => !note.isTrashed && note.type === 'MARKDOWN_NOTE')
|
||||
.forEach(markdownNote => {
|
||||
const folderExportedDir = folderNamesMapping[markdownNote.folder]
|
||||
const snippetName = `${filenamify(markdownNote.title, {
|
||||
replacement: '_'
|
||||
})}.${fileType}`
|
||||
const notePath = path.join(folderExportedDir, snippetName)
|
||||
fs.writeFileSync(notePath, markdownNote.content)
|
||||
})
|
||||
|
||||
return {
|
||||
deduplicators[folder.key] = {}
|
||||
})
|
||||
|
||||
return Promise.all(
|
||||
notes.map(note => {
|
||||
const targetPath = getFilename(
|
||||
note,
|
||||
fileType,
|
||||
folderNamesMapping[note.folder],
|
||||
deduplicators[note.folder]
|
||||
)
|
||||
|
||||
return exportNote(storage.key, note, targetPath, contentFormatter)
|
||||
})
|
||||
).then(() => ({
|
||||
storage,
|
||||
fileType,
|
||||
exportDir
|
||||
}
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
28
browser/main/lib/dataApi/exportTag.js
Normal file
28
browser/main/lib/dataApi/exportTag.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import exportNoteAs from './exportNoteAs'
|
||||
import getFilename from './getFilename'
|
||||
|
||||
/**
|
||||
* @param {Object} data
|
||||
* @param {String} tag
|
||||
* @param {String} fileType
|
||||
* @param {String} exportDir
|
||||
* @param {Object} config
|
||||
*/
|
||||
|
||||
function exportTag(data, tag, fileType, exportDir, config) {
|
||||
const notes = data.noteMap
|
||||
.map(note => note)
|
||||
.filter(note => note.tags.indexOf(tag) !== -1)
|
||||
|
||||
const deduplicator = {}
|
||||
|
||||
return Promise.all(
|
||||
notes.map(note => {
|
||||
const filename = getFilename(note, fileType, exportDir, deduplicator)
|
||||
|
||||
return exportNoteAs(note, filename, fileType, config)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
module.exports = exportTag
|
||||
796
browser/main/lib/dataApi/formatHTML.js
Normal file
796
browser/main/lib/dataApi/formatHTML.js
Normal file
@@ -0,0 +1,796 @@
|
||||
import path from 'path'
|
||||
import fileUrl from 'file-url'
|
||||
import fs from 'fs'
|
||||
import { remote } from 'electron'
|
||||
import consts from 'browser/lib/consts'
|
||||
import Markdown from 'browser/lib/markdown'
|
||||
import attachmentManagement from './attachmentManagement'
|
||||
import { version as codemirrorVersion } from 'codemirror/package.json'
|
||||
import { escapeHtmlCharacters } from 'browser/lib/utils'
|
||||
|
||||
const { app } = remote
|
||||
const appPath = fileUrl(
|
||||
process.env.NODE_ENV === 'production' ? app.getAppPath() : path.resolve()
|
||||
)
|
||||
|
||||
let markdownStyle = ''
|
||||
try {
|
||||
markdownStyle = require('!!css!stylus?sourceMap!../../../components/markdown.styl')[0][1]
|
||||
} catch (e) {}
|
||||
|
||||
export const CSS_FILES = [
|
||||
`${appPath}/node_modules/katex/dist/katex.min.css`,
|
||||
`${appPath}/node_modules/codemirror/lib/codemirror.css`,
|
||||
`${appPath}/node_modules/react-image-carousel/lib/css/main.min.css`
|
||||
]
|
||||
|
||||
const macos = global.process.platform === 'darwin'
|
||||
|
||||
const defaultFontFamily = ['helvetica', 'arial', 'sans-serif']
|
||||
if (!macos) {
|
||||
defaultFontFamily.unshift('Microsoft YaHei')
|
||||
defaultFontFamily.unshift('meiryo')
|
||||
}
|
||||
|
||||
const defaultCodeBlockFontFamily = [
|
||||
'Monaco',
|
||||
'Menlo',
|
||||
'Ubuntu Mono',
|
||||
'Consolas',
|
||||
'source-code-pro',
|
||||
'monospace'
|
||||
]
|
||||
|
||||
function unprefix(file) {
|
||||
if (global.process.platform === 'win32') {
|
||||
return file.replace('file:///', '')
|
||||
} else {
|
||||
return file.replace('file://', '')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ```
|
||||
* {
|
||||
* fontFamily,
|
||||
* fontSize,
|
||||
* lineNumber,
|
||||
* codeBlockFontFamily,
|
||||
* codeBlockTheme,
|
||||
* scrollPastEnd,
|
||||
* theme,
|
||||
* allowCustomCSS,
|
||||
* customCSS
|
||||
* smartQuotes,
|
||||
* sanitize,
|
||||
* breaks,
|
||||
* storagePath,
|
||||
* export,
|
||||
* indentSize
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export default function formatHTML(props) {
|
||||
const {
|
||||
fontFamily,
|
||||
fontSize,
|
||||
codeBlockFontFamily,
|
||||
lineNumber,
|
||||
codeBlockTheme,
|
||||
scrollPastEnd,
|
||||
theme,
|
||||
allowCustomCSS,
|
||||
customCSS
|
||||
} = getStyleParams(props)
|
||||
|
||||
const inlineStyles = buildStyle(
|
||||
fontFamily,
|
||||
fontSize,
|
||||
codeBlockFontFamily,
|
||||
lineNumber,
|
||||
scrollPastEnd,
|
||||
theme,
|
||||
allowCustomCSS,
|
||||
customCSS
|
||||
)
|
||||
|
||||
const { smartQuotes, sanitize, breaks } = props
|
||||
|
||||
let indentSize = parseInt(props.indentSize, 10)
|
||||
if (!(indentSize > 0 && indentSize < 132)) {
|
||||
indentSize = 4
|
||||
}
|
||||
|
||||
const files = [getCodeThemeLink(codeBlockTheme), ...CSS_FILES]
|
||||
|
||||
return function(note, targetPath, exportTasks) {
|
||||
const styles = files
|
||||
.map(file => `<link rel="stylesheet" href="css/${path.basename(file)}">`)
|
||||
.join('\n')
|
||||
|
||||
let inlineScripts = ''
|
||||
let scripts = ''
|
||||
|
||||
let decodeEntities = false
|
||||
function addDecodeEntities() {
|
||||
if (decodeEntities) {
|
||||
return
|
||||
}
|
||||
|
||||
decodeEntities = true
|
||||
|
||||
inlineScripts += `
|
||||
function decodeEntities (text) {
|
||||
var entities = [
|
||||
['apos', '\\''],
|
||||
['amp', '&'],
|
||||
['lt', '<'],
|
||||
['gt', '>'],
|
||||
['#63', '\\?'],
|
||||
['#36', '\\$']
|
||||
]
|
||||
|
||||
for (var i = 0, max = entities.length; i < max; ++i) {
|
||||
text = text.replace(new RegExp(\`&\${entities[i][0]};\`, 'g'), entities[i][1])
|
||||
}
|
||||
|
||||
return text
|
||||
}`
|
||||
}
|
||||
|
||||
let lodash = false
|
||||
function addLodash() {
|
||||
if (lodash) {
|
||||
return
|
||||
}
|
||||
|
||||
lodash = true
|
||||
|
||||
exportTasks.push({
|
||||
src: unprefix(`${appPath}/node_modules/lodash/lodash.min.js`),
|
||||
dst: 'js'
|
||||
})
|
||||
|
||||
scripts += `<script src="js/lodash.min.js"></script>`
|
||||
}
|
||||
|
||||
let raphael = false
|
||||
function addRaphael() {
|
||||
if (raphael) {
|
||||
return
|
||||
}
|
||||
|
||||
raphael = true
|
||||
|
||||
exportTasks.push({
|
||||
src: unprefix(`${appPath}/node_modules/raphael/raphael.min.js`),
|
||||
dst: 'js'
|
||||
})
|
||||
|
||||
scripts += `<script src="js/raphael.min.js"></script>`
|
||||
}
|
||||
|
||||
let yaml = false
|
||||
function addYAML() {
|
||||
if (yaml) {
|
||||
return
|
||||
}
|
||||
|
||||
yaml = true
|
||||
|
||||
exportTasks.push({
|
||||
src: unprefix(`${appPath}/node_modules/js-yaml/dist/js-yaml.min.js`),
|
||||
dst: 'js'
|
||||
})
|
||||
|
||||
scripts += `<script src="js/js-yaml.min.js"></script>`
|
||||
}
|
||||
|
||||
let chart = false
|
||||
function addChart() {
|
||||
if (chart) {
|
||||
return
|
||||
}
|
||||
|
||||
chart = true
|
||||
|
||||
addLodash()
|
||||
|
||||
exportTasks.push({
|
||||
src: unprefix(`${appPath}/node_modules/chart.js/dist/Chart.min.js`),
|
||||
dst: 'js'
|
||||
})
|
||||
|
||||
scripts += `<script src="js/Chart.min.js"></script>`
|
||||
|
||||
inlineScripts += `
|
||||
function displayCharts() {
|
||||
_.forEach(
|
||||
document.querySelectorAll('.chart'),
|
||||
el => {
|
||||
try {
|
||||
const format = el.attributes.getNamedItem('data-format').value
|
||||
const chartConfig = format === 'yaml' ? jsyaml.load(el.innerHTML) : JSON.parse(el.innerHTML)
|
||||
el.innerHTML = ''
|
||||
|
||||
const canvas = document.createElement('canvas')
|
||||
el.appendChild(canvas)
|
||||
|
||||
const height = el.attributes.getNamedItem('data-height')
|
||||
if (height && height.value !== 'undefined') {
|
||||
el.style.height = height.value + 'vh'
|
||||
canvas.height = height.value + 'vh'
|
||||
}
|
||||
|
||||
const chart = new Chart(canvas, chartConfig)
|
||||
} catch (e) {
|
||||
el.className = 'chart-error'
|
||||
el.innerHTML = 'chartjs diagram parse error: ' + e.message
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', displayCharts);
|
||||
`
|
||||
}
|
||||
|
||||
let codemirror = false
|
||||
function addCodeMirror() {
|
||||
if (codemirror) {
|
||||
return
|
||||
}
|
||||
|
||||
codemirror = true
|
||||
|
||||
addDecodeEntities()
|
||||
addLodash()
|
||||
|
||||
exportTasks.push(
|
||||
{
|
||||
src: unprefix(`${appPath}/node_modules/codemirror/lib/codemirror.js`),
|
||||
dst: 'js/codemirror'
|
||||
},
|
||||
{
|
||||
src: unprefix(`${appPath}/node_modules/codemirror/mode/meta.js`),
|
||||
dst: 'js/codemirror/mode'
|
||||
},
|
||||
{
|
||||
src: unprefix(
|
||||
`${appPath}/node_modules/codemirror/addon/mode/loadmode.js`
|
||||
),
|
||||
dst: 'js/codemirror/addon/mode'
|
||||
},
|
||||
{
|
||||
src: unprefix(
|
||||
`${appPath}/node_modules/codemirror/addon/runmode/runmode.js`
|
||||
),
|
||||
dst: 'js/codemirror/addon/runmode'
|
||||
}
|
||||
)
|
||||
|
||||
scripts += `
|
||||
<script src="js/codemirror/codemirror.js"></script>
|
||||
<script src="js/codemirror/mode/meta.js"></script>
|
||||
<script src="js/codemirror/addon/mode/loadmode.js"></script>
|
||||
<script src="js/codemirror/addon/runmode/runmode.js"></script>
|
||||
`
|
||||
|
||||
let className = `cm-s-${codeBlockTheme}`
|
||||
if (codeBlockTheme.indexOf('solarized') === 0) {
|
||||
const [refThema, color] = codeBlockTheme.split(' ')
|
||||
className = `cm-s-${refThema} cm-s-${color}`
|
||||
}
|
||||
|
||||
inlineScripts += `
|
||||
CodeMirror.modeURL = 'https://cdn.jsdelivr.net/npm/codemirror@${codemirrorVersion}/mode/%N/%N.js';
|
||||
|
||||
function displayCodeBlocks() {
|
||||
_.forEach(
|
||||
document.querySelectorAll('.code code'),
|
||||
el => {
|
||||
el.parentNode.className += ' ${className}'
|
||||
let syntax = CodeMirror.findModeByName(el.className)
|
||||
if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text')
|
||||
CodeMirror.requireMode(syntax.mode, () => {
|
||||
const content = decodeEntities(el.innerHTML)
|
||||
el.innerHTML = ''
|
||||
CodeMirror.runMode(content, syntax.mime, el, {
|
||||
tabSize: ${indentSize}
|
||||
})
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', displayCodeBlocks);
|
||||
`
|
||||
}
|
||||
|
||||
let flowchart = false
|
||||
function addFlowchart() {
|
||||
if (flowchart) {
|
||||
return
|
||||
}
|
||||
|
||||
flowchart = true
|
||||
|
||||
addDecodeEntities()
|
||||
addLodash()
|
||||
addRaphael()
|
||||
|
||||
exportTasks.push({
|
||||
src: unprefix(
|
||||
`${appPath}/node_modules/flowchart.js/release/flowchart.min.js`
|
||||
),
|
||||
dst: 'js'
|
||||
})
|
||||
|
||||
scripts += `<script src="js/flowchart.min.js"></script>`
|
||||
|
||||
inlineScripts += `
|
||||
function displayFlowcharts() {
|
||||
_.forEach(
|
||||
document.querySelectorAll('.flowchart'),
|
||||
el => {
|
||||
try {
|
||||
const diagram = flowchart.parse(
|
||||
decodeEntities(el.innerHTML)
|
||||
)
|
||||
el.innerHTML = ''
|
||||
diagram.drawSVG(el)
|
||||
} catch (e) {
|
||||
el.className = 'flowchart-error'
|
||||
el.innerHTML = 'Flowchart parse error: ' + e.message
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', displayFlowcharts);
|
||||
`
|
||||
}
|
||||
|
||||
let mermaid = false
|
||||
function addMermaid() {
|
||||
if (mermaid) {
|
||||
return
|
||||
}
|
||||
|
||||
mermaid = true
|
||||
|
||||
addLodash()
|
||||
|
||||
exportTasks.push({
|
||||
src: unprefix(`${appPath}/node_modules/mermaid/dist/mermaid.min.js`),
|
||||
dst: 'js'
|
||||
})
|
||||
|
||||
scripts += `<script src="js/mermaid.min.js"></script>`
|
||||
|
||||
inlineScripts += `
|
||||
function displayMermaids() {
|
||||
_.forEach(
|
||||
document.querySelectorAll('.mermaid'),
|
||||
el => {
|
||||
const height = el.attributes.getNamedItem('data-height')
|
||||
if (height && height.value !== 'undefined') {
|
||||
el.style.height = height.value + 'vh'
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', displayMermaids);
|
||||
`
|
||||
}
|
||||
|
||||
let sequence = false
|
||||
function addSequence() {
|
||||
if (sequence) {
|
||||
return
|
||||
}
|
||||
|
||||
sequence = true
|
||||
|
||||
addDecodeEntities()
|
||||
addLodash()
|
||||
addRaphael()
|
||||
|
||||
exportTasks.push({
|
||||
src: unprefix(
|
||||
`${appPath}/node_modules/@rokt33r/js-sequence-diagrams/dist/sequence-diagram-min.js`
|
||||
),
|
||||
dst: 'js'
|
||||
})
|
||||
|
||||
scripts += `<script src="js/sequence-diagram-min.js"></script>`
|
||||
|
||||
inlineScripts += `
|
||||
function displaySequences() {
|
||||
_.forEach(
|
||||
document.querySelectorAll('.sequence'),
|
||||
el => {
|
||||
try {
|
||||
const diagram = Diagram.parse(
|
||||
decodeEntities(el.innerHTML)
|
||||
)
|
||||
el.innerHTML = ''
|
||||
diagram.drawSVG(el, { theme: 'simple' })
|
||||
} catch (e) {
|
||||
el.className = 'sequence-error'
|
||||
el.innerHTML = 'Sequence diagram parse error: ' + e.message
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', displaySequences);
|
||||
`
|
||||
}
|
||||
|
||||
const modes = {}
|
||||
const markdown = new Markdown({
|
||||
typographer: smartQuotes,
|
||||
sanitize,
|
||||
breaks,
|
||||
onFence(type, mode) {
|
||||
if (type === 'chart') {
|
||||
addChart()
|
||||
|
||||
if (mode === 'yaml') {
|
||||
addYAML()
|
||||
}
|
||||
} else if (type === 'code') {
|
||||
addCodeMirror()
|
||||
|
||||
if (mode && modes[mode] !== true) {
|
||||
const file = unprefix(
|
||||
`${appPath}/node_modules/codemirror/mode/${mode}/${mode}.js`
|
||||
)
|
||||
|
||||
if (fs.existsSync(file)) {
|
||||
exportTasks.push({
|
||||
src: file,
|
||||
dst: `js/codemirror/mode/${mode}`
|
||||
})
|
||||
|
||||
modes[mode] = true
|
||||
}
|
||||
}
|
||||
} else if (type === 'flowchart') {
|
||||
addFlowchart()
|
||||
} else if (type === 'mermaid') {
|
||||
addMermaid()
|
||||
} else if (type === 'sequence') {
|
||||
addSequence()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
let body = note.content
|
||||
|
||||
if (sanitize === 'NONE') {
|
||||
body = escapeHtmlCharactersInCodeTag(body.split('```'))
|
||||
}
|
||||
|
||||
body = markdown.render(note.content)
|
||||
|
||||
const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent(
|
||||
note.content,
|
||||
props.storagePath
|
||||
)
|
||||
|
||||
files.forEach(file => {
|
||||
exportTasks.push({
|
||||
src: unprefix(file),
|
||||
dst: 'css'
|
||||
})
|
||||
})
|
||||
|
||||
const destinationFolder = props.export.prefixAttachmentFolder
|
||||
? `${path.parse(targetPath).name} - ${
|
||||
attachmentManagement.DESTINATION_FOLDER
|
||||
}`
|
||||
: attachmentManagement.DESTINATION_FOLDER
|
||||
|
||||
attachmentsAbsolutePaths.forEach(attachment => {
|
||||
exportTasks.push({
|
||||
src: attachment,
|
||||
dst: destinationFolder
|
||||
})
|
||||
})
|
||||
|
||||
body = attachmentManagement.replaceStorageReferences(
|
||||
body,
|
||||
note.key,
|
||||
destinationFolder
|
||||
)
|
||||
|
||||
return `
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name = "viewport" content = "width = device-width, initial-scale = 1, maximum-scale = 1">
|
||||
<style id="style">${inlineStyles}</style>
|
||||
${styles}
|
||||
${scripts}
|
||||
<script>${inlineScripts}</script>
|
||||
</head>
|
||||
<body data-theme="${theme}">
|
||||
${body}
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
}
|
||||
}
|
||||
|
||||
export function getStyleParams(props) {
|
||||
const {
|
||||
fontSize,
|
||||
lineNumber,
|
||||
codeBlockTheme,
|
||||
scrollPastEnd,
|
||||
theme,
|
||||
allowCustomCSS,
|
||||
customCSS,
|
||||
RTL
|
||||
} = props
|
||||
|
||||
let { fontFamily, codeBlockFontFamily } = props
|
||||
|
||||
fontFamily =
|
||||
_.isString(fontFamily) && fontFamily.trim().length > 0
|
||||
? fontFamily
|
||||
.split(',')
|
||||
.map(fontName => fontName.trim())
|
||||
.concat(defaultFontFamily)
|
||||
: defaultFontFamily
|
||||
|
||||
codeBlockFontFamily =
|
||||
_.isString(codeBlockFontFamily) && codeBlockFontFamily.trim().length > 0
|
||||
? codeBlockFontFamily
|
||||
.split(',')
|
||||
.map(fontName => fontName.trim())
|
||||
.concat(defaultCodeBlockFontFamily)
|
||||
: defaultCodeBlockFontFamily
|
||||
|
||||
return {
|
||||
fontFamily,
|
||||
fontSize,
|
||||
codeBlockFontFamily,
|
||||
lineNumber,
|
||||
codeBlockTheme,
|
||||
scrollPastEnd,
|
||||
theme,
|
||||
allowCustomCSS,
|
||||
customCSS,
|
||||
RTL
|
||||
}
|
||||
}
|
||||
|
||||
export function getCodeThemeLink(name) {
|
||||
const theme = consts.THEMES.find(theme => theme.name === name)
|
||||
|
||||
return theme != null
|
||||
? theme.path
|
||||
: `${appPath}/node_modules/codemirror/theme/elegant.css`
|
||||
}
|
||||
|
||||
export function buildStyle(
|
||||
fontFamily,
|
||||
fontSize,
|
||||
codeBlockFontFamily,
|
||||
lineNumber,
|
||||
scrollPastEnd,
|
||||
theme,
|
||||
allowCustomCSS,
|
||||
customCSS,
|
||||
RTL
|
||||
) {
|
||||
return `
|
||||
@font-face {
|
||||
font-family: 'Lato';
|
||||
src: url('${appPath}/resources/fonts/Lato-Regular.woff2') format('woff2'), /* Modern Browsers */
|
||||
url('${appPath}/resources/fonts/Lato-Regular.woff') format('woff'), /* Modern Browsers */
|
||||
url('${appPath}/resources/fonts/Lato-Regular.ttf') format('truetype');
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Lato';
|
||||
src: url('${appPath}/resources/fonts/Lato-Black.woff2') format('woff2'), /* Modern Browsers */
|
||||
url('${appPath}/resources/fonts/Lato-Black.woff') format('woff'), /* Modern Browsers */
|
||||
url('${appPath}/resources/fonts/Lato-Black.ttf') format('truetype');
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Material Icons';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Material Icons'),
|
||||
local('MaterialIcons-Regular'),
|
||||
url('${appPath}/resources/fonts/MaterialIcons-Regular.woff2') format('woff2'),
|
||||
url('${appPath}/resources/fonts/MaterialIcons-Regular.woff') format('woff'),
|
||||
url('${appPath}/resources/fonts/MaterialIcons-Regular.ttf') format('truetype');
|
||||
}
|
||||
${markdownStyle}
|
||||
|
||||
body {
|
||||
font-family: '${fontFamily.join("','")}';
|
||||
font-size: ${fontSize}px;
|
||||
${scrollPastEnd && 'padding-bottom: 90vh;box-sizing: border-box;'}
|
||||
${RTL && 'direction: rtl;text-align: right;'}
|
||||
}
|
||||
@media print {
|
||||
body {
|
||||
padding-bottom: initial;
|
||||
}
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: '${codeBlockFontFamily.join("','")}';
|
||||
background-color: rgba(0,0,0,0.04);
|
||||
text-align: left;
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
p code.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
|
||||
}
|
||||
103
browser/main/lib/dataApi/formatMarkdown.js
Normal file
103
browser/main/lib/dataApi/formatMarkdown.js
Normal file
@@ -0,0 +1,103 @@
|
||||
import attachmentManagement from './attachmentManagement'
|
||||
import yaml from 'js-yaml'
|
||||
import path from 'path'
|
||||
|
||||
const delimiterRegExp = /^\-{3}/
|
||||
|
||||
/**
|
||||
* ```
|
||||
* {
|
||||
* storagePath,
|
||||
* export
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export default function formatMarkdown(props) {
|
||||
return function(note, targetPath, exportTasks) {
|
||||
let result = note.content
|
||||
|
||||
if (props.storagePath && note.key) {
|
||||
const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent(
|
||||
result,
|
||||
props.storagePath
|
||||
)
|
||||
|
||||
const destinationFolder = props.export.prefixAttachmentFolder
|
||||
? `${path.parse(targetPath).name} - ${
|
||||
attachmentManagement.DESTINATION_FOLDER
|
||||
}`
|
||||
: attachmentManagement.DESTINATION_FOLDER
|
||||
|
||||
attachmentsAbsolutePaths.forEach(attachment => {
|
||||
exportTasks.push({
|
||||
src: attachment,
|
||||
dst: destinationFolder
|
||||
})
|
||||
})
|
||||
|
||||
result = attachmentManagement.replaceStorageReferences(
|
||||
result,
|
||||
note.key,
|
||||
destinationFolder
|
||||
)
|
||||
}
|
||||
|
||||
if (props.export.metadata === 'MERGE_HEADER') {
|
||||
const metadata = getFrontMatter(result)
|
||||
|
||||
const values = Object.assign({}, note)
|
||||
delete values.content
|
||||
delete values.isTrashed
|
||||
|
||||
for (const key in values) {
|
||||
metadata[key] = values[key]
|
||||
}
|
||||
|
||||
result = replaceFrontMatter(result, metadata)
|
||||
} else if (props.export.metadata === 'MERGE_VARIABLE') {
|
||||
const metadata = getFrontMatter(result)
|
||||
|
||||
const values = Object.assign({}, note)
|
||||
delete values.content
|
||||
delete values.isTrashed
|
||||
|
||||
if (props.export.variable) {
|
||||
metadata[props.export.variable] = values
|
||||
} else {
|
||||
for (const key in values) {
|
||||
metadata[key] = values[key]
|
||||
}
|
||||
}
|
||||
|
||||
result = replaceFrontMatter(result, metadata)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
function getFrontMatter(markdown) {
|
||||
const lines = markdown.split('\n')
|
||||
|
||||
if (delimiterRegExp.test(lines[0])) {
|
||||
let line = 0
|
||||
while (++line < lines.length && !delimiterRegExp.test(lines[line])) {}
|
||||
|
||||
return yaml.load(lines.slice(1, line).join('\n')) || {}
|
||||
} else {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
function replaceFrontMatter(markdown, metadata) {
|
||||
const lines = markdown.split('\n')
|
||||
|
||||
if (delimiterRegExp.test(lines[0])) {
|
||||
let line = 0
|
||||
while (++line < lines.length && !delimiterRegExp.test(lines[line])) {}
|
||||
|
||||
return `---\n${yaml.dump(metadata)}---\n${lines.slice(line + 1).join('\n')}`
|
||||
} else {
|
||||
return `---\n${yaml.dump(metadata)}---\n\n${markdown}`
|
||||
}
|
||||
}
|
||||
26
browser/main/lib/dataApi/formatPDF.js
Normal file
26
browser/main/lib/dataApi/formatPDF.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import formatHTML from './formatHTML'
|
||||
import { remote } from 'electron'
|
||||
|
||||
export default function formatPDF(props) {
|
||||
return function(note, targetPath, exportTasks) {
|
||||
const printout = new remote.BrowserWindow({
|
||||
show: false,
|
||||
webPreferences: { webSecurity: false, javascript: false }
|
||||
})
|
||||
|
||||
printout.loadURL(
|
||||
'data:text/html;charset=UTF-8,' +
|
||||
formatHTML(props)(note, targetPath, exportTasks)
|
||||
)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
printout.webContents.on('did-finish-load', () => {
|
||||
printout.webContents.printToPDF({}, (err, data) => {
|
||||
if (err) reject(err)
|
||||
else resolve(data)
|
||||
printout.destroy()
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
58
browser/main/lib/dataApi/getContentFormatter.js
Normal file
58
browser/main/lib/dataApi/getContentFormatter.js
Normal file
@@ -0,0 +1,58 @@
|
||||
import formatMarkdown from './formatMarkdown'
|
||||
import formatHTML from './formatHTML'
|
||||
import formatPDF from './formatPDF'
|
||||
|
||||
/**
|
||||
* @param {Object} storage
|
||||
* @param {String} fileType
|
||||
* @param {Object} config
|
||||
*/
|
||||
|
||||
export default function getContentFormatter(storage, fileType, config) {
|
||||
if (fileType === 'md') {
|
||||
return formatMarkdown({
|
||||
storagePath: storage.path,
|
||||
export: config.export
|
||||
})
|
||||
} else if (fileType === 'html') {
|
||||
return formatHTML({
|
||||
theme: config.ui.theme,
|
||||
fontSize: config.preview.fontSize,
|
||||
fontFamily: config.preview.fontFamily,
|
||||
codeBlockTheme: config.preview.codeBlockTheme,
|
||||
codeBlockFontFamily: config.editor.fontFamily,
|
||||
lineNumber: config.preview.lineNumber,
|
||||
indentSize: config.editor.indentSize,
|
||||
scrollPastEnd: config.preview.scrollPastEnd,
|
||||
smartQuotes: config.preview.smartQuotes,
|
||||
breaks: config.preview.breaks,
|
||||
sanitize: config.preview.sanitize,
|
||||
customCSS: config.preview.customCSS,
|
||||
allowCustomCSS: config.preview.allowCustomCSS,
|
||||
storagePath: storage.path,
|
||||
export: config.export,
|
||||
RTL: config.editor.rtlEnabled /* && this.state.RTL */
|
||||
})
|
||||
} else if (fileType === 'pdf') {
|
||||
return formatPDF({
|
||||
theme: config.ui.theme,
|
||||
fontSize: config.preview.fontSize,
|
||||
fontFamily: config.preview.fontFamily,
|
||||
codeBlockTheme: config.preview.codeBlockTheme,
|
||||
codeBlockFontFamily: config.editor.fontFamily,
|
||||
lineNumber: config.preview.lineNumber,
|
||||
indentSize: config.editor.indentSize,
|
||||
scrollPastEnd: config.preview.scrollPastEnd,
|
||||
smartQuotes: config.preview.smartQuotes,
|
||||
breaks: config.preview.breaks,
|
||||
sanitize: config.preview.sanitize,
|
||||
customCSS: config.preview.customCSS,
|
||||
allowCustomCSS: config.preview.allowCustomCSS,
|
||||
storagePath: storage.path,
|
||||
export: config.export,
|
||||
RTL: config.editor.rtlEnabled /* && this.state.RTL */
|
||||
})
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
37
browser/main/lib/dataApi/getFilename.js
Normal file
37
browser/main/lib/dataApi/getFilename.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import filenamify from 'filenamify'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
import path from 'path'
|
||||
|
||||
/**
|
||||
* @param {Object} note
|
||||
* @param {String} fileType
|
||||
* @param {String} directory
|
||||
* @param {Object} deduplicator
|
||||
*
|
||||
* @return {String}
|
||||
*/
|
||||
|
||||
function getFilename(note, fileType, directory, deduplicator) {
|
||||
const basename = note.title
|
||||
? filenamify(note.title, { replacement: '_' })
|
||||
: i18n.__('Untitled')
|
||||
|
||||
if (deduplicator) {
|
||||
if (deduplicator[basename]) {
|
||||
const filename = path.join(
|
||||
directory,
|
||||
`${basename} (${deduplicator[basename]}).${fileType}`
|
||||
)
|
||||
|
||||
++deduplicator[basename]
|
||||
|
||||
return filename
|
||||
} else {
|
||||
deduplicator[basename] = 1
|
||||
}
|
||||
}
|
||||
|
||||
return path.join(directory, `${basename}.${fileType}`)
|
||||
}
|
||||
|
||||
module.exports = getFilename
|
||||
@@ -15,11 +15,14 @@ const dataApi = {
|
||||
updateNote: require('./updateNote'),
|
||||
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'),
|
||||
|
||||
@@ -1,5 +1,109 @@
|
||||
@import('./Tab')
|
||||
|
||||
.container
|
||||
display flex
|
||||
flex-direction column
|
||||
align-items center
|
||||
justify-content center
|
||||
position relative
|
||||
margin-bottom 2em
|
||||
margin-left 2em
|
||||
|
||||
.box-minmax
|
||||
width 608px
|
||||
height 45px
|
||||
display flex
|
||||
justify-content space-between
|
||||
font-size $tab--button-font-size
|
||||
color $ui-text-color
|
||||
span first-child
|
||||
margin-top 18px
|
||||
padding-right 10px
|
||||
padding-left 10px
|
||||
padding-top 8px
|
||||
position relative
|
||||
border $ui-borderColor
|
||||
border-radius 5px
|
||||
background $ui-backgroundColor
|
||||
|
||||
div[id^="secondRow"]
|
||||
position absolute
|
||||
z-index 2
|
||||
left 0
|
||||
top 0
|
||||
margin-bottom -42px
|
||||
.rs-label
|
||||
margin-left -20px
|
||||
|
||||
div[id^="firstRow"]
|
||||
position absolute
|
||||
z-index 2
|
||||
left 0
|
||||
top 0
|
||||
margin-bottom -25px
|
||||
.rs-range
|
||||
&::-webkit-slider-thumb
|
||||
margin-top 0px
|
||||
transform rotate(180deg)
|
||||
.rs-label
|
||||
margin-bottom -85px
|
||||
margin-top 85px
|
||||
|
||||
|
||||
.rs-range
|
||||
margin-top 29px
|
||||
width 600px
|
||||
-webkit-appearance none
|
||||
&:focus
|
||||
outline black
|
||||
&::-webkit-slider-runnable-track
|
||||
width 100%
|
||||
height 0.1px
|
||||
cursor pointer
|
||||
box-shadow none
|
||||
background $ui-backgroundColor
|
||||
border-radius 0px
|
||||
border 0px solid #010101
|
||||
cursor none
|
||||
|
||||
&::-webkit-slider-thumb
|
||||
box-shadow none
|
||||
border 1px solid $ui-borderColor
|
||||
box-shadow 0px 10px 10px rgba(0, 0, 0, 0.25)
|
||||
height 32px
|
||||
width 32px
|
||||
border-radius 22px
|
||||
background white
|
||||
cursor pointer
|
||||
-webkit-appearance none
|
||||
margin-top -20px
|
||||
border-color $ui-default-button-backgroundColor
|
||||
height 32px
|
||||
border-top-left-radius 10%
|
||||
border-top-right-radius 10%
|
||||
|
||||
.rs-label
|
||||
position relative
|
||||
transform-origin center center
|
||||
display block
|
||||
background transparent
|
||||
border-radius none
|
||||
line-height 30px
|
||||
font-weight normal
|
||||
box-sizing border-box
|
||||
border none
|
||||
margin-bottom -5px
|
||||
margin-top -10px
|
||||
clear both
|
||||
float left
|
||||
height 17px
|
||||
margin-left -25px
|
||||
left attr(value)
|
||||
color $ui-text-color
|
||||
font-style normal
|
||||
font-weight normal
|
||||
line-height normal
|
||||
font-size $tab--button-font-size
|
||||
.root
|
||||
padding 15px
|
||||
margin-bottom 30px
|
||||
@@ -35,6 +139,13 @@
|
||||
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
|
||||
@@ -175,6 +286,9 @@ body[data-theme="dark"]
|
||||
.group-section-control
|
||||
select, .group-section-control-input
|
||||
colorDarkControl()
|
||||
.rs-label
|
||||
color $ui-dark-text-color
|
||||
|
||||
|
||||
apply-theme(theme)
|
||||
body[data-theme={theme}]
|
||||
@@ -205,6 +319,8 @@ apply-theme(theme)
|
||||
.group-section-control
|
||||
select, .group-section-control-input
|
||||
colorThemedControl(theme)
|
||||
.rs-label
|
||||
color get-theme-var(theme, 'text-color')
|
||||
|
||||
for theme in 'solarized-dark' 'dracula'
|
||||
apply-theme(theme)
|
||||
|
||||
184
browser/main/modals/PreferencesModal/ExportTab.js
Normal file
184
browser/main/modals/PreferencesModal/ExportTab.js
Normal file
@@ -0,0 +1,184 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './ConfigTab.styl'
|
||||
import ConfigManager from 'browser/main/lib/ConfigManager'
|
||||
import store from 'browser/main/store'
|
||||
import _ from 'lodash'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
|
||||
const electron = require('electron')
|
||||
const ipc = electron.ipcRenderer
|
||||
|
||||
class ExportTab extends React.Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
config: props.config
|
||||
}
|
||||
}
|
||||
|
||||
clearMessage() {
|
||||
_.debounce(() => {
|
||||
this.setState({
|
||||
ExportAlert: null
|
||||
})
|
||||
}, 2000)()
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.handleSettingDone = () => {
|
||||
this.setState({
|
||||
ExportAlert: {
|
||||
type: 'success',
|
||||
message: i18n.__('Successfully applied!')
|
||||
}
|
||||
})
|
||||
}
|
||||
this.handleSettingError = err => {
|
||||
this.setState({
|
||||
ExportAlert: {
|
||||
type: 'error',
|
||||
message:
|
||||
err.message != null ? err.message : i18n.__('An error occurred!')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
this.oldExport = this.state.config.export
|
||||
|
||||
ipc.addListener('APP_SETTING_DONE', this.handleSettingDone)
|
||||
ipc.addListener('APP_SETTING_ERROR', this.handleSettingError)
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
ipc.removeListener('APP_SETTING_DONE', this.handleSettingDone)
|
||||
ipc.removeListener('APP_SETTING_ERROR', this.handleSettingError)
|
||||
}
|
||||
|
||||
handleSaveButtonClick(e) {
|
||||
const newConfig = {
|
||||
export: this.state.config.export
|
||||
}
|
||||
|
||||
ConfigManager.set(newConfig)
|
||||
|
||||
store.dispatch({
|
||||
type: 'SET_UI',
|
||||
config: newConfig
|
||||
})
|
||||
|
||||
this.clearMessage()
|
||||
this.props.haveToSave()
|
||||
}
|
||||
|
||||
handleExportChange(e) {
|
||||
const { config } = this.state
|
||||
|
||||
config.export = {
|
||||
metadata: this.refs.metadata.value,
|
||||
variable: !_.isNil(this.refs.variable)
|
||||
? this.refs.variable.value
|
||||
: config.export.variable,
|
||||
prefixAttachmentFolder: this.refs.prefixAttachmentFolder.checked
|
||||
}
|
||||
|
||||
this.setState({
|
||||
config
|
||||
})
|
||||
|
||||
if (_.isEqual(this.oldExport, config.export)) {
|
||||
this.props.haveToSave()
|
||||
} else {
|
||||
this.props.haveToSave({
|
||||
tab: 'Export',
|
||||
type: 'warning',
|
||||
message: i18n.__('Unsaved Changes!')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { config, ExportAlert } = this.state
|
||||
|
||||
const ExportAlertElement =
|
||||
ExportAlert != null ? (
|
||||
<p className={`alert ${ExportAlert.type}`}>{ExportAlert.message}</p>
|
||||
) : null
|
||||
|
||||
return (
|
||||
<div styleName='root'>
|
||||
<div styleName='group'>
|
||||
<div styleName='group-header'>{i18n.__('Export')}</div>
|
||||
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label'>{i18n.__('Metadata')}</div>
|
||||
<div styleName='group-section-control'>
|
||||
<select
|
||||
value={config.export.metadata}
|
||||
onChange={e => this.handleExportChange(e)}
|
||||
ref='metadata'
|
||||
>
|
||||
<option value='DONT_EXPORT'>{i18n.__(`Don't export`)}</option>
|
||||
<option value='MERGE_HEADER'>
|
||||
{i18n.__('Merge with the header')}
|
||||
</option>
|
||||
<option value='MERGE_VARIABLE'>
|
||||
{i18n.__('Merge with a variable')}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{config.export.metadata === 'MERGE_VARIABLE' && (
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label'>
|
||||
{i18n.__('Variable Name')}
|
||||
</div>
|
||||
<div styleName='group-section-control'>
|
||||
<input
|
||||
styleName='group-section-control-input'
|
||||
onChange={e => this.handleExportChange(e)}
|
||||
ref='variable'
|
||||
value={config.export.variable}
|
||||
type='text'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div styleName='group-checkBoxSection'>
|
||||
<label>
|
||||
<input
|
||||
onChange={e => this.handleExportChange(e)}
|
||||
checked={config.export.prefixAttachmentFolder}
|
||||
ref='prefixAttachmentFolder'
|
||||
type='checkbox'
|
||||
/>
|
||||
|
||||
{i18n.__('Prefix attachment folder')}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div styleName='group-control'>
|
||||
<button
|
||||
styleName='group-control-rightButton'
|
||||
onClick={e => this.handleSaveButtonClick(e)}
|
||||
>
|
||||
{i18n.__('Save')}
|
||||
</button>
|
||||
{ExportAlertElement}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ExportTab.propTypes = {
|
||||
dispatch: PropTypes.func,
|
||||
haveToSave: PropTypes.func
|
||||
}
|
||||
|
||||
export default CSSModules(ExportTab, styles)
|
||||
@@ -16,25 +16,78 @@ class InfoTab extends React.Component {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
config: this.props.config
|
||||
config: this.props.config,
|
||||
subscriptionFormStatus: 'idle',
|
||||
subscriptionFormErrorMessage: null,
|
||||
subscriptionFormEmail: ''
|
||||
}
|
||||
}
|
||||
|
||||
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 }
|
||||
const newConfig = {
|
||||
amaEnabled: this.refs.amaEnabled.checked,
|
||||
autoUpdateEnabled: this.refs.autoUpdateEnabled.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 = {
|
||||
amaEnabled: this.state.config.amaEnabled
|
||||
}
|
||||
const newConfig = this.state.config
|
||||
|
||||
if (!newConfig.amaEnabled) {
|
||||
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('DISABLE_AMA')
|
||||
@@ -61,20 +114,17 @@ 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'>
|
||||
@@ -134,6 +184,40 @@ 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'>
|
||||
@@ -145,9 +229,7 @@ class InfoTab extends React.Component {
|
||||
height='92'
|
||||
/>
|
||||
<div styleName='icon-right'>
|
||||
<div styleName='appId'>
|
||||
{i18n.__('Boostnote')} {appVersion}
|
||||
</div>
|
||||
<div styleName='appId'>Boostnote Legacy {appVersion}</div>
|
||||
<div styleName='description'>
|
||||
{i18n.__(
|
||||
'An open source note-taking app made for programmers just like you.'
|
||||
@@ -183,7 +265,8 @@ class InfoTab extends React.Component {
|
||||
<label>
|
||||
<input
|
||||
type='checkbox'
|
||||
onChange={this.toggleAutoUpdate.bind(this)}
|
||||
ref='autoUpdateEnabled'
|
||||
onChange={() => this.handleAutoUpdateChange()}
|
||||
checked={this.state.config.autoUpdateEnabled}
|
||||
/>
|
||||
{i18n.__('Enable Auto Update')}
|
||||
|
||||
@@ -33,6 +33,35 @@
|
||||
.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
|
||||
|
||||
207
browser/main/modals/PreferencesModal/PluginsTab.js
Normal file
207
browser/main/modals/PreferencesModal/PluginsTab.js
Normal file
@@ -0,0 +1,207 @@
|
||||
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'
|
||||
import { sync as commandExists } from 'command-exists'
|
||||
const electron = require('electron')
|
||||
const ipc = electron.ipcRenderer
|
||||
const { remote } = electron
|
||||
const { dialog } = remote
|
||||
class PluginsTab extends React.Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
config: props.config
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.handleSettingDone = () => {
|
||||
this.setState({
|
||||
pluginsAlert: {
|
||||
type: 'success',
|
||||
message: i18n.__('Successfully applied!')
|
||||
}
|
||||
})
|
||||
}
|
||||
this.handleSettingError = err => {
|
||||
this.setState({
|
||||
pluginsAlert: {
|
||||
type: 'error',
|
||||
message:
|
||||
err.message != null ? err.message : i18n.__('An error occurred!')
|
||||
}
|
||||
})
|
||||
}
|
||||
this.oldWakatimeConfig = this.state.config.wakatime
|
||||
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)
|
||||
}
|
||||
|
||||
checkWakatimePluginRequirement() {
|
||||
const { wakatime } = this.state.config
|
||||
if (wakatime.isActive && !commandExists('wakatime')) {
|
||||
this.setState({
|
||||
wakatimePluginAlert: {
|
||||
type: i18n.__('Warning'),
|
||||
message: i18n.__('Missing wakatime cli')
|
||||
}
|
||||
})
|
||||
|
||||
const alertConfig = {
|
||||
type: 'warning',
|
||||
message: i18n.__('Missing Wakatime CLI'),
|
||||
detail: i18n.__(
|
||||
`Please install Wakatime CLI to use Wakatime tracker feature.`
|
||||
),
|
||||
buttons: [i18n.__('OK')]
|
||||
}
|
||||
dialog.showMessageBox(remote.getCurrentWindow(), alertConfig)
|
||||
} else {
|
||||
this.setState({
|
||||
wakatimePluginAlert: null
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handleSaveButtonClick(e) {
|
||||
const newConfig = {
|
||||
wakatime: {
|
||||
isActive: this.state.config.wakatime.isActive,
|
||||
key: this.state.config.wakatime.key
|
||||
}
|
||||
}
|
||||
|
||||
ConfigManager.set(newConfig)
|
||||
|
||||
store.dispatch({
|
||||
type: 'SET_CONFIG',
|
||||
config: newConfig
|
||||
})
|
||||
this.clearMessage()
|
||||
this.props.haveToSave()
|
||||
this.checkWakatimePluginRequirement()
|
||||
}
|
||||
|
||||
handleIsWakatimePluginActiveChange(e) {
|
||||
const { config } = this.state
|
||||
config.wakatime.isActive = !config.wakatime.isActive
|
||||
this.setState({
|
||||
config
|
||||
})
|
||||
if (_.isEqual(this.oldWakatimeConfig.isActive, config.wakatime.isActive)) {
|
||||
this.props.haveToSave()
|
||||
} else {
|
||||
this.props.haveToSave({
|
||||
tab: 'Plugins',
|
||||
type: 'warning',
|
||||
message: i18n.__('Unsaved Changes!')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handleWakatimeKeyChange(e) {
|
||||
const { config } = this.state
|
||||
config.wakatime = {
|
||||
isActive: true,
|
||||
key: this.refs.wakatimeKey.value
|
||||
}
|
||||
this.setState({
|
||||
config
|
||||
})
|
||||
if (_.isEqual(this.oldWakatimeConfig.key, config.wakatime.key)) {
|
||||
this.props.haveToSave()
|
||||
} else {
|
||||
this.props.haveToSave({
|
||||
tab: 'Plugins',
|
||||
type: 'warning',
|
||||
message: i18n.__('Unsaved Changes!')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
clearMessage() {
|
||||
_.debounce(() => {
|
||||
this.setState({
|
||||
pluginsAlert: null
|
||||
})
|
||||
}, 2000)()
|
||||
}
|
||||
|
||||
render() {
|
||||
const pluginsAlert = this.state.pluginsAlert
|
||||
const pluginsAlertElement =
|
||||
pluginsAlert != null ? (
|
||||
<p className={`alert ${pluginsAlert.type}`}>{pluginsAlert.message}</p>
|
||||
) : null
|
||||
|
||||
const wakatimeAlert = this.state.wakatimePluginAlert
|
||||
const wakatimePluginAlertElement =
|
||||
wakatimeAlert != null ? (
|
||||
<p className={`alert ${wakatimeAlert.type}`}>{wakatimeAlert.message}</p>
|
||||
) : null
|
||||
|
||||
const { config } = this.state
|
||||
|
||||
return (
|
||||
<div styleName='root'>
|
||||
<div styleName='group'>
|
||||
<div styleName='group-header'>{i18n.__('Plugins')}</div>
|
||||
<div styleName='group-header2'>{i18n.__('Wakatime')}</div>
|
||||
<div styleName='group-checkBoxSection'>
|
||||
<label>
|
||||
<input
|
||||
onChange={e => this.handleIsWakatimePluginActiveChange(e)}
|
||||
checked={config.wakatime.isActive}
|
||||
ref='wakatimeIsActive'
|
||||
type='checkbox'
|
||||
/>
|
||||
|
||||
{i18n.__('Enable Wakatime')}
|
||||
</label>
|
||||
</div>
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label'>{i18n.__('Wakatime key')}</div>
|
||||
<div styleName='group-section-control'>
|
||||
<input
|
||||
styleName='group-section-control-input'
|
||||
onChange={e => this.handleWakatimeKeyChange(e)}
|
||||
disabled={!config.wakatime.isActive}
|
||||
ref='wakatimeKey'
|
||||
value={config.wakatime.key}
|
||||
type='text'
|
||||
/>
|
||||
{wakatimePluginAlertElement}
|
||||
</div>
|
||||
</div>
|
||||
<div styleName='group-control'>
|
||||
<button
|
||||
styleName='group-control-rightButton'
|
||||
onClick={e => this.handleSaveButtonClick(e)}
|
||||
>
|
||||
{i18n.__('Save')}
|
||||
</button>
|
||||
{pluginsAlertElement}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
PluginsTab.propTypes = {
|
||||
dispatch: PropTypes.func,
|
||||
haveToSave: PropTypes.func
|
||||
}
|
||||
|
||||
export default CSSModules(PluginsTab, styles)
|
||||
@@ -35,10 +35,18 @@ class SnippetEditor extends React.Component {
|
||||
foldGutter: true,
|
||||
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
|
||||
autoCloseBrackets: {
|
||||
pairs: this.props.matchingPairs,
|
||||
triples: this.props.matchingTriples,
|
||||
explode: this.props.explodingPairs,
|
||||
override: true
|
||||
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
|
||||
}
|
||||
},
|
||||
mode: 'null'
|
||||
})
|
||||
|
||||
@@ -152,8 +152,15 @@ 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
|
||||
|
||||
@@ -13,6 +13,7 @@ import i18n from 'browser/lib/i18n'
|
||||
import { getLanguages } from 'browser/lib/Languages'
|
||||
import normalizeEditorFontFamily from 'browser/lib/normalizeEditorFontFamily'
|
||||
import uiThemes from 'browser/lib/ui-themes'
|
||||
import { chooseTheme, applyTheme } from 'browser/main/lib/ThemeManager'
|
||||
|
||||
const OSX = global.process.platform === 'darwin'
|
||||
|
||||
@@ -84,6 +85,11 @@ class UiTab extends React.Component {
|
||||
const newConfig = {
|
||||
ui: {
|
||||
theme: this.refs.uiTheme.value,
|
||||
defaultTheme: this.refs.uiTheme.value,
|
||||
enableScheduleTheme: this.refs.enableScheduleTheme.checked,
|
||||
scheduledTheme: this.refs.uiScheduledTheme.value,
|
||||
scheduleStart: this.refs.scheduleStart.value,
|
||||
scheduleEnd: this.refs.scheduleEnd.value,
|
||||
language: this.refs.uiLanguage.value,
|
||||
defaultNote: this.refs.defaultNote.value,
|
||||
tagNewNoteWithFilteringTags: this.refs.tagNewNoteWithFilteringTags
|
||||
@@ -118,14 +124,21 @@ 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
|
||||
@@ -190,6 +203,9 @@ class UiTab extends React.Component {
|
||||
preview: this.state.config.preview
|
||||
}
|
||||
|
||||
chooseTheme(newConfig)
|
||||
applyTheme(newConfig.ui.theme)
|
||||
|
||||
ConfigManager.set(newConfig)
|
||||
|
||||
store.dispatch({
|
||||
@@ -208,6 +224,21 @@ class UiTab extends React.Component {
|
||||
}, 2000)()
|
||||
}
|
||||
|
||||
formatTime(time) {
|
||||
let hour = Math.floor(time / 60)
|
||||
let minute = time % 60
|
||||
|
||||
if (hour < 10) {
|
||||
hour = '0' + hour
|
||||
}
|
||||
|
||||
if (minute < 10) {
|
||||
minute = '0' + minute
|
||||
}
|
||||
|
||||
return `${hour}:${minute}`
|
||||
}
|
||||
|
||||
render() {
|
||||
const UiAlert = this.state.UiAlert
|
||||
const UiAlertElement =
|
||||
@@ -232,7 +263,7 @@ class UiTab extends React.Component {
|
||||
</div>
|
||||
<div styleName='group-section-control'>
|
||||
<select
|
||||
value={config.ui.theme}
|
||||
value={config.ui.defaultTheme}
|
||||
onChange={e => this.handleUIChange(e)}
|
||||
ref='uiTheme'
|
||||
>
|
||||
@@ -263,6 +294,101 @@ class UiTab extends React.Component {
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div styleName='group-header2'>{i18n.__('Theme Schedule')}</div>
|
||||
<div styleName='group-checkBoxSection'>
|
||||
<label>
|
||||
<input
|
||||
onChange={e => this.handleUIChange(e)}
|
||||
checked={config.ui.enableScheduleTheme}
|
||||
ref='enableScheduleTheme'
|
||||
type='checkbox'
|
||||
/>
|
||||
|
||||
{i18n.__('Enable Scheduled Themes')}
|
||||
</label>
|
||||
</div>
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label'>
|
||||
{i18n.__('Scheduled Theme')}
|
||||
</div>
|
||||
<div styleName='group-section-control'>
|
||||
<select
|
||||
disabled={!config.ui.enableScheduleTheme}
|
||||
value={config.ui.scheduledTheme}
|
||||
onChange={e => this.handleUIChange(e)}
|
||||
ref='uiScheduledTheme'
|
||||
>
|
||||
<optgroup label='Light Themes'>
|
||||
{uiThemes
|
||||
.filter(theme => !theme.isDark)
|
||||
.sort((a, b) => a.label.localeCompare(b.label))
|
||||
.map(theme => {
|
||||
return (
|
||||
<option value={theme.name} key={theme.name}>
|
||||
{theme.label}
|
||||
</option>
|
||||
)
|
||||
})}
|
||||
</optgroup>
|
||||
<optgroup label='Dark Themes'>
|
||||
{uiThemes
|
||||
.filter(theme => theme.isDark)
|
||||
.sort((a, b) => a.label.localeCompare(b.label))
|
||||
.map(theme => {
|
||||
return (
|
||||
<option value={theme.name} key={theme.name}>
|
||||
{theme.label}
|
||||
</option>
|
||||
)
|
||||
})}
|
||||
</optgroup>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div styleName='group-section'>
|
||||
<div styleName='container'>
|
||||
<div id='firstRow'>
|
||||
<span
|
||||
id='rs-bullet-1'
|
||||
styleName='rs-label'
|
||||
>{`End: ${this.formatTime(config.ui.scheduleEnd)}`}</span>
|
||||
<input
|
||||
disabled={!config.ui.enableScheduleTheme}
|
||||
id='rs-range-line-1'
|
||||
styleName='rs-range'
|
||||
type='range'
|
||||
value={config.ui.scheduleEnd}
|
||||
min='0'
|
||||
max='1440'
|
||||
step='5'
|
||||
ref='scheduleEnd'
|
||||
onChange={e => this.handleUIChange(e)}
|
||||
/>
|
||||
</div>
|
||||
<div id='secondRow'>
|
||||
<span
|
||||
id='rs-bullet-2'
|
||||
styleName='rs-label'
|
||||
>{`Start: ${this.formatTime(config.ui.scheduleStart)}`}</span>
|
||||
<input
|
||||
disabled={!config.ui.enableScheduleTheme}
|
||||
id='rs-range-line-2'
|
||||
styleName='rs-range'
|
||||
type='range'
|
||||
value={config.ui.scheduleStart}
|
||||
min='0'
|
||||
max='1440'
|
||||
step='5'
|
||||
ref='scheduleStart'
|
||||
onChange={e => this.handleUIChange(e)}
|
||||
/>
|
||||
</div>
|
||||
<div styleName='box-minmax'>
|
||||
<span>00:00</span>
|
||||
<span>24:00</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label'>{i18n.__('Language')}</div>
|
||||
@@ -626,6 +752,126 @@ 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
|
||||
@@ -756,50 +1002,19 @@ class UiTab extends React.Component {
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label'>
|
||||
{i18n.__('Matching character pairs')}
|
||||
</div>
|
||||
<div styleName='group-section-control'>
|
||||
<div styleName='group-checkBoxSection'>
|
||||
<label>
|
||||
<input
|
||||
styleName='group-section-control-input'
|
||||
value={this.state.config.editor.matchingPairs}
|
||||
ref='matchingPairs'
|
||||
onChange={e => this.handleUIChange(e)}
|
||||
type='text'
|
||||
checked={this.state.config.editor.dateFormatISO8601}
|
||||
ref='dateFormatISO8601'
|
||||
type='checkbox'
|
||||
/>
|
||||
</div>
|
||||
|
||||
{i18n.__('Date shortcut use iso 8601 format')}
|
||||
</label>
|
||||
</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,9 @@ 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'
|
||||
import ModalEscButton from 'browser/components/ModalEscButton'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
@@ -23,7 +25,8 @@ class Preferences extends React.Component {
|
||||
currentTab: 'STORAGES',
|
||||
UIAlert: '',
|
||||
HotkeyAlert: '',
|
||||
BlogAlert: ''
|
||||
BlogAlert: '',
|
||||
ExportAlert: ''
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,8 +83,25 @@ 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':
|
||||
return (
|
||||
<PluginsTab
|
||||
dispatch={dispatch}
|
||||
config={config}
|
||||
haveToSave={alert => this.setState({ PluginsAlert: alert })}
|
||||
/>
|
||||
)
|
||||
case 'STORAGES':
|
||||
default:
|
||||
return (
|
||||
@@ -122,7 +142,13 @@ 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: 'SNIPPET', label: i18n.__('Snippets') }
|
||||
{
|
||||
target: 'EXPORT',
|
||||
label: i18n.__('Export'),
|
||||
Export: this.state.ExportAlert
|
||||
},
|
||||
{ target: 'SNIPPET', label: i18n.__('Snippets') },
|
||||
{ target: 'PLUGINS', label: i18n.__('Plugins') }
|
||||
]
|
||||
|
||||
const navButtons = tabs.map(tab => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './RenameFolderModal.styl'
|
||||
import styles from './RenameModal.styl'
|
||||
import dataApi from 'browser/main/lib/dataApi'
|
||||
import { store } from 'browser/main/store'
|
||||
import ModalEscButton from 'browser/components/ModalEscButton'
|
||||
|
||||
@@ -46,13 +46,18 @@
|
||||
font-size 14px
|
||||
colorPrimaryButton()
|
||||
|
||||
.error
|
||||
text-align center
|
||||
color #F44336
|
||||
height 20px
|
||||
|
||||
apply-theme(theme)
|
||||
body[data-theme={theme}]
|
||||
.root
|
||||
background-color transparent
|
||||
|
||||
.header
|
||||
background-color get-theme-var(theme, 'button--hover-backgroundColor')
|
||||
background-color transparent
|
||||
border-color get-theme-var(theme, 'borderColor')
|
||||
color get-theme-var(theme, 'text-color')
|
||||
|
||||
196
browser/main/modals/RenameTagModal.js
Normal file
196
browser/main/modals/RenameTagModal.js
Normal file
@@ -0,0 +1,196 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './RenameModal.styl'
|
||||
import dataApi from 'browser/main/lib/dataApi'
|
||||
import ModalEscButton from 'browser/components/ModalEscButton'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
import { replace } from 'connected-react-router'
|
||||
import ee from 'browser/main/lib/eventEmitter'
|
||||
import { isEmpty } from 'lodash'
|
||||
import electron from 'electron'
|
||||
|
||||
const { remote } = electron
|
||||
const { dialog } = remote
|
||||
|
||||
class RenameTagModal extends React.Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.nameInput = null
|
||||
|
||||
this.handleChange = this.handleChange.bind(this)
|
||||
|
||||
this.setTextInputRef = el => {
|
||||
this.nameInput = el
|
||||
}
|
||||
|
||||
this.state = {
|
||||
name: props.tagName,
|
||||
oldName: props.tagName
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.nameInput.focus()
|
||||
this.nameInput.select()
|
||||
}
|
||||
|
||||
handleChange(e) {
|
||||
this.setState({
|
||||
name: this.nameInput.value,
|
||||
showerror: false,
|
||||
errormessage: ''
|
||||
})
|
||||
}
|
||||
|
||||
handleKeyDown(e) {
|
||||
if (e.keyCode === 27) {
|
||||
this.props.close()
|
||||
}
|
||||
}
|
||||
|
||||
handleInputKeyDown(e) {
|
||||
switch (e.keyCode) {
|
||||
case 13:
|
||||
this.handleConfirm()
|
||||
}
|
||||
}
|
||||
|
||||
handleConfirm() {
|
||||
if (this.state.name.trim().length > 0) {
|
||||
const { name, oldName } = this.state
|
||||
this.renameTag(oldName, name)
|
||||
}
|
||||
}
|
||||
|
||||
showError(message) {
|
||||
this.setState({
|
||||
showerror: true,
|
||||
errormessage: message
|
||||
})
|
||||
}
|
||||
|
||||
renameTag(tag, updatedTag) {
|
||||
const { data, dispatch } = this.props
|
||||
|
||||
if (tag === updatedTag) {
|
||||
// confirm with-out any change - just dismiss the modal
|
||||
this.props.close()
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
data.noteMap
|
||||
.map(note => note)
|
||||
.some(note => note.tags.indexOf(updatedTag) !== -1)
|
||||
) {
|
||||
const alertConfig = {
|
||||
type: 'warning',
|
||||
message: i18n.__('Confirm tag merge'),
|
||||
detail: i18n.__(
|
||||
`Tag ${tag} will be merged with existing tag ${updatedTag}`
|
||||
),
|
||||
buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
|
||||
}
|
||||
|
||||
const dialogButtonIndex = dialog.showMessageBox(
|
||||
remote.getCurrentWindow(),
|
||||
alertConfig
|
||||
)
|
||||
|
||||
if (dialogButtonIndex === 1) {
|
||||
return // bail early on cancel click
|
||||
}
|
||||
}
|
||||
|
||||
const notes = data.noteMap
|
||||
.map(note => note)
|
||||
.filter(
|
||||
note => note.tags.indexOf(tag) !== -1 && note.tags.indexOf(updatedTag)
|
||||
)
|
||||
.map(note => {
|
||||
note = Object.assign({}, note)
|
||||
note.tags = note.tags.slice()
|
||||
|
||||
note.tags[note.tags.indexOf(tag)] = updatedTag
|
||||
|
||||
return note
|
||||
})
|
||||
|
||||
if (isEmpty(notes)) {
|
||||
this.showError(i18n.__('Tag exists'))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
Promise.all(
|
||||
notes.map(note => dataApi.updateNote(note.storage, note.key, note))
|
||||
)
|
||||
.then(updatedNotes => {
|
||||
updatedNotes.forEach(note => {
|
||||
dispatch({
|
||||
type: 'UPDATE_NOTE',
|
||||
note
|
||||
})
|
||||
})
|
||||
})
|
||||
.then(() => {
|
||||
if (window.location.hash.includes(tag)) {
|
||||
dispatch(replace(`/tags/${updatedTag}`))
|
||||
}
|
||||
ee.emit('sidebar:rename-tag', { tag, updatedTag })
|
||||
this.props.close()
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
const { close } = this.props
|
||||
const { errormessage } = this.state
|
||||
|
||||
return (
|
||||
<div
|
||||
styleName='root'
|
||||
tabIndex='-1'
|
||||
onKeyDown={e => this.handleKeyDown(e)}
|
||||
>
|
||||
<div styleName='header'>
|
||||
<div styleName='title'>{i18n.__('Rename Tag')}</div>
|
||||
</div>
|
||||
<ModalEscButton handleEscButtonClick={close} />
|
||||
|
||||
<div styleName='control'>
|
||||
<input
|
||||
styleName='control-input'
|
||||
placeholder={i18n.__('Tag Name')}
|
||||
ref={this.setTextInputRef}
|
||||
value={this.state.name}
|
||||
onChange={this.handleChange}
|
||||
onKeyDown={e => this.handleInputKeyDown(e)}
|
||||
/>
|
||||
<button
|
||||
styleName='control-confirmButton'
|
||||
onClick={() => this.handleConfirm()}
|
||||
>
|
||||
{i18n.__('Confirm')}
|
||||
</button>
|
||||
</div>
|
||||
<div className='error' styleName='error'>
|
||||
{errormessage}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
RenameTagModal.propTypes = {
|
||||
storage: PropTypes.shape({
|
||||
key: PropTypes.string
|
||||
}),
|
||||
folder: PropTypes.shape({
|
||||
key: PropTypes.string,
|
||||
name: PropTypes.string
|
||||
})
|
||||
}
|
||||
|
||||
export default CSSModules(RenameTagModal, styles)
|
||||
196
extra_scripts/codemirror/addon/edit/closebrackets.js
vendored
Normal file
196
extra_scripts/codemirror/addon/edit/closebrackets.js
vendored
Normal file
@@ -0,0 +1,196 @@
|
||||
// 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)))
|
||||
}
|
||||
});
|
||||
@@ -1,22 +1,31 @@
|
||||
(function (mod) {
|
||||
if (typeof exports === 'object' && typeof module === 'object') { // Common JS
|
||||
;(function(mod) {
|
||||
if (typeof exports === 'object' && typeof module === 'object') {
|
||||
// Common JS
|
||||
mod(require('../codemirror/lib/codemirror'))
|
||||
} else if (typeof define === 'function' && define.amd) { // AMD
|
||||
} else if (typeof define === 'function' && define.amd) {
|
||||
// AMD
|
||||
define(['../codemirror/lib/codemirror'], mod)
|
||||
} else { // Plain browser env
|
||||
} else {
|
||||
// Plain browser env
|
||||
mod(CodeMirror)
|
||||
}
|
||||
})(function (CodeMirror) {
|
||||
})(function(CodeMirror) {
|
||||
'use strict'
|
||||
|
||||
const shell = require('electron').shell
|
||||
const remote = require('electron').remote
|
||||
const eventEmitter = {
|
||||
emit: function() {
|
||||
remote.getCurrentWindow().webContents.send.apply(null, arguments)
|
||||
}
|
||||
}
|
||||
const yOffset = 2
|
||||
|
||||
const macOS = global.process.platform === 'darwin'
|
||||
const modifier = macOS ? 'metaKey' : 'ctrlKey'
|
||||
|
||||
class HyperLink {
|
||||
constructor (cm) {
|
||||
constructor(cm) {
|
||||
this.cm = cm
|
||||
this.lineDiv = cm.display.lineDiv
|
||||
|
||||
@@ -28,11 +37,16 @@
|
||||
this.tooltip = document.createElement('div')
|
||||
this.tooltipContent = document.createElement('div')
|
||||
this.tooltipIndicator = document.createElement('div')
|
||||
this.tooltip.setAttribute('class', 'CodeMirror-hover CodeMirror-matchingbracket CodeMirror-selected')
|
||||
this.tooltip.setAttribute(
|
||||
'class',
|
||||
'CodeMirror-hover CodeMirror-matchingbracket CodeMirror-selected'
|
||||
)
|
||||
this.tooltip.setAttribute('cm-ignore-events', 'true')
|
||||
this.tooltip.appendChild(this.tooltipContent)
|
||||
this.tooltip.appendChild(this.tooltipIndicator)
|
||||
this.tooltipContent.textContent = `${macOS ? 'Cmd(⌘)' : 'Ctrl(^)'} + click to follow link`
|
||||
this.tooltipContent.textContent = `${
|
||||
macOS ? 'Cmd(⌘)' : 'Ctrl(^)'
|
||||
} + click to follow link`
|
||||
|
||||
this.lineDiv.addEventListener('mousedown', this.onMouseDown)
|
||||
this.lineDiv.addEventListener('mouseenter', this.onMouseEnter, {
|
||||
@@ -47,11 +61,20 @@
|
||||
passive: true
|
||||
})
|
||||
}
|
||||
getUrl (el) {
|
||||
getUrl(el) {
|
||||
const className = el.className.split(' ')
|
||||
|
||||
if (className.indexOf('cm-url') !== -1) {
|
||||
const match = /^\((.*)\)|\[(.*)\]|(.*)$/.exec(el.textContent)
|
||||
// multiple cm-url because of search term
|
||||
const cmUrlSpans = Array.from(
|
||||
el.parentNode.getElementsByClassName('cm-url')
|
||||
)
|
||||
const textContent =
|
||||
cmUrlSpans.length > 1
|
||||
? cmUrlSpans.map(span => span.textContent).join('')
|
||||
: el.textContent
|
||||
|
||||
const match = /^\((.*)\)|\[(.*)\]|(.*)$/.exec(textContent)
|
||||
const url = match[1] || match[2] || match[3]
|
||||
|
||||
// `:storage` is the value of the variable `STORAGE_FOLDER_PLACEHOLDER` defined in `browser/main/lib/dataApi/attachmentManagement`
|
||||
@@ -60,26 +83,106 @@
|
||||
|
||||
return null
|
||||
}
|
||||
onMouseDown (e) {
|
||||
specialLinkHandler(e, rawHref, linkHash) {
|
||||
const isStartWithHash = rawHref[0] === '#'
|
||||
|
||||
const extractIdRegex = /file:\/\/.*main.?\w*.html#/ // file://path/to/main(.development.)html
|
||||
const regexNoteInternalLink = new RegExp(`${extractIdRegex.source}(.+)`)
|
||||
if (isStartWithHash || regexNoteInternalLink.test(rawHref)) {
|
||||
const posOfHash = linkHash.indexOf('#')
|
||||
if (posOfHash > -1) {
|
||||
const extractedId = linkHash.slice(posOfHash + 1)
|
||||
const targetId = mdurl.encode(extractedId)
|
||||
const targetElement = document.getElementById(targetId) // this.getWindow().document.getElementById(targetId)
|
||||
|
||||
if (targetElement != null) {
|
||||
this.scrollTo(0, targetElement.offsetTop)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// this will match the new uuid v4 hash and the old hash
|
||||
// e.g.
|
||||
// :note:1c211eb7dcb463de6490 and
|
||||
// :note:7dd23275-f2b4-49cb-9e93-3454daf1af9c
|
||||
const regexIsNoteLink = /^:note:([a-zA-Z0-9-]{20,36})$/
|
||||
if (regexIsNoteLink.test(linkHash)) {
|
||||
eventEmitter.emit('list:jump', linkHash.replace(':note:', ''))
|
||||
return
|
||||
}
|
||||
|
||||
const regexIsLine = /^:line:[0-9]/
|
||||
if (regexIsLine.test(linkHash)) {
|
||||
const numberPattern = /\d+/g
|
||||
|
||||
const lineNumber = parseInt(linkHash.match(numberPattern)[0])
|
||||
eventEmitter.emit('line:jump', lineNumber)
|
||||
return
|
||||
}
|
||||
|
||||
// this will match the old link format storage.key-note.key
|
||||
// e.g.
|
||||
// 877f99c3268608328037-1c211eb7dcb463de6490
|
||||
const regexIsLegacyNoteLink = /^(.{20})-(.{20})$/
|
||||
if (regexIsLegacyNoteLink.test(linkHash)) {
|
||||
eventEmitter.emit('list:jump', linkHash.split('-')[1])
|
||||
return
|
||||
}
|
||||
|
||||
const regexIsTagLink = /^:tag:([\w]+)$/
|
||||
if (regexIsTagLink.test(rawHref)) {
|
||||
const tag = rawHref.match(regexIsTagLink)[1]
|
||||
eventEmitter.emit('dispatch:push', `/tags/${encodeURIComponent(tag)}`)
|
||||
return
|
||||
}
|
||||
}
|
||||
onMouseDown(e) {
|
||||
const { target } = e
|
||||
if (!e[modifier]) {
|
||||
return
|
||||
}
|
||||
|
||||
// Create URL spans array used for special case "search term is hitting a link".
|
||||
const cmUrlSpans = Array.from(
|
||||
e.target.parentNode.getElementsByClassName('cm-url')
|
||||
)
|
||||
|
||||
const innerText =
|
||||
cmUrlSpans.length > 1
|
||||
? cmUrlSpans.map(span => span.textContent).join('')
|
||||
: e.target.innerText
|
||||
const rawHref = innerText.trim().slice(1, -1) // get link text from markdown text
|
||||
|
||||
if (!rawHref) return // not checked href because parser will create file://... string for [empty link]()
|
||||
|
||||
const parser = document.createElement('a')
|
||||
parser.href = rawHref
|
||||
const { href, hash } = parser
|
||||
|
||||
const linkHash = hash === '' ? rawHref : hash // needed because we're having special link formats that are removed by parser e.g. :line:10
|
||||
|
||||
this.specialLinkHandler(target, rawHref, linkHash)
|
||||
|
||||
const url = this.getUrl(target)
|
||||
|
||||
// all special cases handled --> other case
|
||||
if (url) {
|
||||
e.preventDefault()
|
||||
|
||||
shell.openExternal(url)
|
||||
}
|
||||
}
|
||||
onMouseEnter (e) {
|
||||
onMouseEnter(e) {
|
||||
const { target } = e
|
||||
|
||||
const url = this.getUrl(target)
|
||||
if (url) {
|
||||
if (e[modifier]) {
|
||||
target.classList.add('CodeMirror-activeline-background', 'CodeMirror-hyperlink')
|
||||
target.classList.add(
|
||||
'CodeMirror-activeline-background',
|
||||
'CodeMirror-hyperlink'
|
||||
)
|
||||
} else {
|
||||
target.classList.add('CodeMirror-activeline-background')
|
||||
}
|
||||
@@ -87,14 +190,17 @@
|
||||
this.showInfo(target)
|
||||
}
|
||||
}
|
||||
onMouseLeave (e) {
|
||||
onMouseLeave(e) {
|
||||
if (this.tooltip.parentElement === this.lineDiv) {
|
||||
e.target.classList.remove('CodeMirror-activeline-background', 'CodeMirror-hyperlink')
|
||||
e.target.classList.remove(
|
||||
'CodeMirror-activeline-background',
|
||||
'CodeMirror-hyperlink'
|
||||
)
|
||||
|
||||
this.lineDiv.removeChild(this.tooltip)
|
||||
}
|
||||
}
|
||||
onMouseMove (e) {
|
||||
onMouseMove(e) {
|
||||
if (this.tooltip.parentElement === this.lineDiv) {
|
||||
if (e[modifier]) {
|
||||
e.target.classList.add('CodeMirror-hyperlink')
|
||||
@@ -103,25 +209,25 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
showInfo (relatedTo) {
|
||||
showInfo(relatedTo) {
|
||||
const b1 = relatedTo.getBoundingClientRect()
|
||||
const b2 = this.lineDiv.getBoundingClientRect()
|
||||
const tdiv = this.tooltip
|
||||
|
||||
tdiv.style.left = (b1.left - b2.left) + 'px'
|
||||
tdiv.style.left = b1.left - b2.left + 'px'
|
||||
this.lineDiv.appendChild(tdiv)
|
||||
|
||||
const b3 = tdiv.getBoundingClientRect()
|
||||
const top = b1.top - b2.top - b3.height - yOffset
|
||||
if (top < 0) {
|
||||
tdiv.style.top = (b1.top - b2.top + b1.height + yOffset) + 'px'
|
||||
tdiv.style.top = b1.top - b2.top + b1.height + yOffset + 'px'
|
||||
} else {
|
||||
tdiv.style.top = top + 'px'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CodeMirror.defineOption('hyperlink', true, (cm) => {
|
||||
CodeMirror.defineOption('hyperlink', true, cm => {
|
||||
const addon = new HyperLink(cm)
|
||||
})
|
||||
})
|
||||
|
||||
73
extra_scripts/codemirror/mode/bfm/bfm.js
vendored
73
extra_scripts/codemirror/mode/bfm/bfm.js
vendored
@@ -1,10 +1,20 @@
|
||||
(function(mod) {
|
||||
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||
mod(require("../codemirror/lib/codemirror"), require("../codemirror/mode/gfm/gfm"), require("../codemirror/mode/yaml-frontmatter/yaml-frontmatter"))
|
||||
else if (typeof define == "function" && define.amd) // AMD
|
||||
define(["../codemirror/lib/codemirror", "../codemirror/mode/gfm/gfm", "../codemirror/mode/yaml-frontmatter/yaml-frontmatter"], mod)
|
||||
else // Plain browser env
|
||||
mod(CodeMirror)
|
||||
;(function(mod) {
|
||||
if (typeof exports == 'object' && typeof module == 'object')
|
||||
// CommonJS
|
||||
mod(
|
||||
require('../codemirror/lib/codemirror'),
|
||||
require('../codemirror/mode/gfm/gfm'),
|
||||
require('../codemirror/mode/yaml-frontmatter/yaml-frontmatter')
|
||||
)
|
||||
else if (typeof define == 'function' && define.amd)
|
||||
// AMD
|
||||
define([
|
||||
'../codemirror/lib/codemirror',
|
||||
'../codemirror/mode/gfm/gfm',
|
||||
'../codemirror/mode/yaml-frontmatter/yaml-frontmatter'
|
||||
], mod)
|
||||
// Plain browser env
|
||||
else mod(CodeMirror)
|
||||
})(function(CodeMirror) {
|
||||
'use strict'
|
||||
|
||||
@@ -87,18 +97,23 @@
|
||||
token: function(stream, state) {
|
||||
const initialPos = stream.pos
|
||||
|
||||
if (state.fencedEndRE && stream.match(state.fencedEndRE)) {
|
||||
state.fencedEndRE = null
|
||||
state.fencedMode = null
|
||||
state.fencedState = null
|
||||
if (state.fencedEndRE) {
|
||||
if (stream.match(state.fencedEndRE)) {
|
||||
state.fencedEndRE = null
|
||||
state.fencedMode = null
|
||||
state.fencedState = null
|
||||
|
||||
stream.pos = initialPos
|
||||
stream.pos = initialPos
|
||||
} else if (state.fencedMode) {
|
||||
return state.fencedMode.token(stream, state.fencedState)
|
||||
} else {
|
||||
state.overlayCur = this.overlayToken(stream, state)
|
||||
state.overlayPos = stream.pos
|
||||
|
||||
return state.overlayCur
|
||||
}
|
||||
}
|
||||
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] + '+ *$')
|
||||
@@ -141,30 +156,10 @@
|
||||
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.localMode = getMode(match[2], match[3], config, stream.lineOracle.doc.cm)
|
||||
if (state.localMode) {
|
||||
state.localState = CodeMirror.startState(state.localMode)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
state.combineTokens = true
|
||||
|
||||
if (state.inTable) {
|
||||
@@ -226,8 +221,8 @@
|
||||
CodeMirror.defineMIME('text/x-bfm', 'bfm')
|
||||
|
||||
CodeMirror.modeInfo.push({
|
||||
name: "Boost Flavored Markdown",
|
||||
mime: "text/x-bfm",
|
||||
mode: "bfm"
|
||||
name: 'Boost Flavored Markdown',
|
||||
mime: 'text/x-bfm',
|
||||
mode: 'bfm'
|
||||
})
|
||||
})
|
||||
157
extra_scripts/codemirror/mode/gfm/gfm.js
vendored
Normal file
157
extra_scripts/codemirror/mode/gfm/gfm.js
vendored
Normal file
@@ -0,0 +1,157 @@
|
||||
// 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('../codemirror/lib/codemirror'),
|
||||
require('../codemirror/mode/markdown/markdown'),
|
||||
require('../codemirror/addon/mode/overlay')
|
||||
)
|
||||
else if (typeof define == 'function' && define.amd)
|
||||
// AMD
|
||||
define([
|
||||
'../codemirror/lib/codemirror',
|
||||
'../codemirror/mode/markdown/markdown',
|
||||
'../codemirror/addon/mode/overlay'
|
||||
], mod)
|
||||
// Plain browser env
|
||||
else mod(CodeMirror)
|
||||
})(function(CodeMirror) {
|
||||
'use strict'
|
||||
|
||||
var urlRE = /^((?:(?:aaas?|about|acap|adiumxtra|af[ps]|aim|apt|attachment|aw|beshare|bitcoin|bolo|callto|cap|chrome(?:-extension)?|cid|coap|com-eventbrite-attendee|content|crid|cvs|data|dav|dict|dlna-(?:playcontainer|playsingle)|dns|doi|dtn|dvb|ed2k|facetime|feed|file|finger|fish|ftp|geo|gg|git|gizmoproject|go|gopher|gtalk|h323|hcp|https?|iax|icap|icon|im|imap|info|ipn|ipp|irc[6s]?|iris(?:\.beep|\.lwz|\.xpc|\.xpcs)?|itms|jar|javascript|jms|keyparc|lastfm|ldaps?|magnet|mailto|maps|market|message|mid|mms|ms-help|msnim|msrps?|mtqp|mumble|mupdate|mvn|news|nfs|nih?|nntp|notes|oid|opaquelocktoken|palm|paparazzi|platform|pop|pres|proxy|psyc|query|res(?:ource)?|rmi|rsync|rtmp|rtsp|secondlife|service|session|sftp|sgn|shttp|sieve|sips?|skype|sm[bs]|snmp|soap\.beeps?|soldat|spotify|ssh|steam|svn|teamspeak|tel(?:net)?|tftp|things|thismessage|tip|tn3270|tv|udp|unreal|urn|ut2004|vemmi|ventrilo|view-source|webcal|wss?|wtai|wyciwyg|xcon(?:-userid)?|xfire|xmlrpc\.beeps?|xmpp|xri|ymsgr|z39\.50[rs]?):(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]|\([^\s()<>]*\))+(?:\([^\s()<>]*\)|[^\s`*!()\[\]{};:'".,<>?«»“”‘’]))/i
|
||||
|
||||
CodeMirror.defineMode(
|
||||
'gfm',
|
||||
function(config, modeConfig) {
|
||||
var codeDepth = 0
|
||||
function blankLine(state) {
|
||||
state.code = false
|
||||
return null
|
||||
}
|
||||
var gfmOverlay = {
|
||||
startState: function() {
|
||||
return {
|
||||
code: false,
|
||||
codeBlock: false,
|
||||
ateSpace: false
|
||||
}
|
||||
},
|
||||
copyState: function(s) {
|
||||
return {
|
||||
code: s.code,
|
||||
codeBlock: s.codeBlock,
|
||||
ateSpace: s.ateSpace
|
||||
}
|
||||
},
|
||||
token: function(stream, state) {
|
||||
state.combineTokens = null
|
||||
|
||||
// Hack to prevent formatting override inside code blocks (block and inline)
|
||||
if (state.codeBlock) {
|
||||
if (stream.match(/^```+/)) {
|
||||
state.codeBlock = false
|
||||
return null
|
||||
}
|
||||
stream.skipToEnd()
|
||||
return null
|
||||
}
|
||||
if (stream.sol()) {
|
||||
state.code = false
|
||||
}
|
||||
if (stream.sol() && stream.match(/^```+/)) {
|
||||
stream.skipToEnd()
|
||||
state.codeBlock = true
|
||||
return null
|
||||
}
|
||||
// If this block is changed, it may need to be updated in Markdown mode
|
||||
if (stream.peek() === '`') {
|
||||
stream.next()
|
||||
var before = stream.pos
|
||||
stream.eatWhile('`')
|
||||
var difference = 1 + stream.pos - before
|
||||
if (!state.code) {
|
||||
codeDepth = difference
|
||||
state.code = true
|
||||
} else {
|
||||
if (difference === codeDepth) {
|
||||
// Must be exact
|
||||
state.code = false
|
||||
}
|
||||
}
|
||||
return null
|
||||
} else if (state.code) {
|
||||
stream.next()
|
||||
return null
|
||||
}
|
||||
// Check if space. If so, links can be formatted later on
|
||||
if (stream.eatSpace()) {
|
||||
state.ateSpace = true
|
||||
return null
|
||||
}
|
||||
if (stream.sol() || state.ateSpace) {
|
||||
state.ateSpace = false
|
||||
if (modeConfig.gitHubSpice !== false) {
|
||||
if (
|
||||
stream.match(
|
||||
/^(?:[a-zA-Z0-9\-_]+\/)?(?:[a-zA-Z0-9\-_]+@)?(?=.{0,6}\d)(?:[a-f0-9]{7,40}\b)/
|
||||
)
|
||||
) {
|
||||
// User/Project@SHA
|
||||
// User@SHA
|
||||
// SHA
|
||||
state.combineTokens = true
|
||||
return 'link'
|
||||
} else if (
|
||||
stream.match(
|
||||
/^(?:[a-zA-Z0-9\-_]+\/)?(?:[a-zA-Z0-9\-_]+)?#[0-9]+\b/
|
||||
)
|
||||
) {
|
||||
// User/Project#Num
|
||||
// User#Num
|
||||
// #Num
|
||||
state.combineTokens = true
|
||||
return 'link'
|
||||
}
|
||||
}
|
||||
}
|
||||
if (
|
||||
stream.match(urlRE) &&
|
||||
stream.string.slice(stream.start - 2, stream.start) != '](' &&
|
||||
(stream.start == 0 ||
|
||||
/\W/.test(stream.string.charAt(stream.start - 1)))
|
||||
) {
|
||||
// URLs
|
||||
// Taken from http://daringfireball.net/2010/07/improved_regex_for_matching_urls
|
||||
// And then (issue #1160) simplified to make it not crash the Chrome Regexp engine
|
||||
// And then limited url schemes to the CommonMark list, so foo:bar isn't matched as a URL
|
||||
state.combineTokens = true
|
||||
return 'link'
|
||||
}
|
||||
stream.next()
|
||||
return null
|
||||
},
|
||||
blankLine: blankLine
|
||||
}
|
||||
|
||||
var markdownConfig = {
|
||||
taskLists: true,
|
||||
strikethrough: true,
|
||||
emoji: true
|
||||
}
|
||||
for (var attr in modeConfig) {
|
||||
markdownConfig[attr] = modeConfig[attr]
|
||||
}
|
||||
markdownConfig.name = 'markdown'
|
||||
return CodeMirror.overlayMode(
|
||||
CodeMirror.getMode(config, markdownConfig),
|
||||
gfmOverlay
|
||||
)
|
||||
},
|
||||
'markdown'
|
||||
)
|
||||
|
||||
CodeMirror.defineMIME('text/x-gfm', 'gfm')
|
||||
})
|
||||
@@ -187,7 +187,7 @@ module.exports = function(grunt) {
|
||||
}
|
||||
|
||||
ChildProcess.exec(
|
||||
`codesign --verbose --deep --force --sign \"${OSX_COMMON_NAME}\" dist/Boostnote-darwin-x64/Boostnote.app`,
|
||||
`codesign --verbose --deep --force --timestamp=none --sign \"${OSX_COMMON_NAME}\" dist/Boostnote-darwin-x64/Boostnote.app`,
|
||||
function(err, stdout, stderr) {
|
||||
grunt.log.writeln(stdout)
|
||||
if (err) {
|
||||
|
||||
@@ -26,6 +26,7 @@ if (!singleInstance) {
|
||||
}
|
||||
|
||||
var isUpdateReady = false
|
||||
let updateFound = false
|
||||
|
||||
var ghReleasesOpts = {
|
||||
repo: 'BoostIO/boost-releases',
|
||||
@@ -36,25 +37,33 @@ const updater = new GhReleases(ghReleasesOpts)
|
||||
|
||||
// Check for updates
|
||||
// `status` returns true if there is a new update available
|
||||
function checkUpdate() {
|
||||
function checkUpdate(manualTriggered = false) {
|
||||
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
|
||||
}
|
||||
if (!electronConfig.get('autoUpdateEnabled', true)) return
|
||||
if (process.platform === 'linux' || isUpdateReady) {
|
||||
|
||||
// End if auto updates disabled and it is an automatic check
|
||||
if (!electronConfig.get('autoUpdateEnabled', true) && !manualTriggered) return
|
||||
|
||||
if (process.platform === 'linux' || isUpdateReady || updateFound) {
|
||||
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!')
|
||||
updater.download()
|
||||
updateFound = true
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -63,6 +72,7 @@ updater.on('update-downloaded', info => {
|
||||
if (mainWindow != null) {
|
||||
mainWindow.webContents.send('update-ready', 'Update available!')
|
||||
isUpdateReady = true
|
||||
updateFound = false
|
||||
}
|
||||
})
|
||||
|
||||
@@ -77,6 +87,14 @@ 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()
|
||||
})
|
||||
@@ -113,7 +131,7 @@ app.on('ready', function() {
|
||||
if (isUpdateReady) {
|
||||
mainWindow.webContents.send('update-ready', 'Update available!')
|
||||
} else {
|
||||
checkUpdate()
|
||||
checkUpdate(msg === 'manual')
|
||||
}
|
||||
})
|
||||
}, 10 * 1000)
|
||||
|
||||
@@ -178,6 +178,18 @@ const file = {
|
||||
mainWindow.webContents.send('list:isMarkdownNote', 'print')
|
||||
mainWindow.webContents.send('print')
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: 'Update',
|
||||
click() {
|
||||
mainWindow.webContents.send('update')
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -314,6 +326,12 @@ const view = {
|
||||
mainWindow.webContents.send('editor:fullscreen')
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Toggle Editor Orientation',
|
||||
click() {
|
||||
mainWindow.webContents.send('editor:orientation')
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
@@ -466,9 +484,21 @@ 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, help]
|
||||
? [boost, file, edit, view, window, team, help]
|
||||
: process.platform === 'win32'
|
||||
? [boost, file, view, help]
|
||||
: [file, view, help]
|
||||
? [boost, file, view, team, help]
|
||||
: [file, view, team, 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;
|
||||
@@ -108,15 +108,15 @@
|
||||
<script src="../node_modules/codemirror/addon/display/panel.js"></script>
|
||||
<script src="../node_modules/codemirror/mode/xml/xml.js"></script>
|
||||
<script src="../node_modules/codemirror/mode/markdown/markdown.js"></script>
|
||||
<script src="../node_modules/codemirror/mode/gfm/gfm.js"></script>
|
||||
<script src="../node_modules/codemirror/mode/yaml/yaml.js"></script>
|
||||
<script src="../node_modules/codemirror/mode/yaml-frontmatter/yaml-frontmatter.js"></script>
|
||||
|
||||
<script src="../extra_scripts/boost/boostNewLineIndentContinueMarkdownList.js"></script>
|
||||
<script src="../extra_scripts/codemirror/mode/bfm/bfm.js"></script>
|
||||
<script src="../extra_scripts/codemirror/mode/gfm/gfm.js"></script>
|
||||
<script src="../extra_scripts/codemirror/addon/hyperlink/hyperlink.js"></script>
|
||||
|
||||
<script src="../node_modules/codemirror/addon/edit/closebrackets.js"></script>
|
||||
<script src="../extra_scripts/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>
|
||||
|
||||
@@ -104,15 +104,15 @@
|
||||
<script src="../node_modules/codemirror/addon/display/panel.js"></script>
|
||||
<script src="../node_modules/codemirror/mode/xml/xml.js"></script>
|
||||
<script src="../node_modules/codemirror/mode/markdown/markdown.js"></script>
|
||||
<script src="../node_modules/codemirror/mode/gfm/gfm.js"></script>
|
||||
<script src="../node_modules/codemirror/mode/yaml/yaml.js"></script>
|
||||
<script src="../node_modules/codemirror/mode/yaml-frontmatter/yaml-frontmatter.js"></script>
|
||||
|
||||
<script src="../extra_scripts/boost/boostNewLineIndentContinueMarkdownList.js"></script>
|
||||
<script src="../extra_scripts/codemirror/mode/bfm/bfm.js"></script>
|
||||
<script src="../extra_scripts/codemirror/mode/gfm/gfm.js"></script>
|
||||
<script src="../extra_scripts/codemirror/addon/hyperlink/hyperlink.js"></script>
|
||||
|
||||
<script src="../node_modules/codemirror/addon/edit/closebrackets.js"></script>
|
||||
<script src="../extra_scripts/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,7 +202,6 @@
|
||||
"Create new folder": "Ordner erstellen",
|
||||
"Folder name": "Ordnername",
|
||||
"Create": "Erstellen",
|
||||
"Untitled": "Neuer Ordner",
|
||||
"Unlink Storage": "Speicherverknüpfung aufheben",
|
||||
"Unlinking removes this linked storage from Boostnote. No data is removed, please manually delete the folder from your hard drive if needed.": "Die Verknüpfung des Speichers mit Boostnote wird entfernt. Es werden keine Daten gelöscht. Um die Daten dauerhaft zu löschen musst du den Ordner auf der Festplatte manuell entfernen.",
|
||||
"Empty note": "Leere Notiz",
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"Ctrl(^)": "Ctrl(^)",
|
||||
"to create a new note": "新增筆記",
|
||||
"Toggle Mode": "切換模式",
|
||||
"Add tag...": "新增標籤...",
|
||||
"Trash": "垃圾桶",
|
||||
"Ok": "好",
|
||||
"MODIFICATION DATE": "修改時間",
|
||||
@@ -22,50 +23,57 @@
|
||||
".pdf": ".pdf",
|
||||
"Print": "列印",
|
||||
"Your preferences for Boostnote": "Boostnote 偏好設定",
|
||||
"Help": "幫助",
|
||||
"Hide Help": "隱藏幫助",
|
||||
"Storage Locations": "儲存空間",
|
||||
"Add Storage Location": "新增儲存位置",
|
||||
"Add Folder": "新增資料夾",
|
||||
"Select Folder": "選擇資料夾",
|
||||
"Open Storage folder": "開啟儲存資料夾",
|
||||
"Unlink": "解除連結",
|
||||
"Edit": "編輯",
|
||||
"Delete": "刪除",
|
||||
"Interface": "界面",
|
||||
"Interface Theme": "主題",
|
||||
"Interface": "介面",
|
||||
"Interface Theme": "介面主題",
|
||||
"Default": "預設",
|
||||
"White": "White",
|
||||
"Solarized Dark": "Solarized Dark",
|
||||
"Dark": "Dark",
|
||||
"Show a confirmation dialog when deleting notes": "刪除筆記的時候,顯示確認對話框",
|
||||
"Show a confirmation dialog when deleting notes": "刪除筆記時顯示確認對話框",
|
||||
"Disable Direct Write (It will be applied after restarting)": "停用直接編輯 (重新啟動後生效)",
|
||||
"Show only related tags": "只顯示相關標籤",
|
||||
"Editor Theme": "編輯器主題",
|
||||
"Editor Font Size": "編輯器字型大小",
|
||||
"Editor Font Family": "編輯器字體",
|
||||
"Editor Indent Style": "縮排風格",
|
||||
"Spaces": "空格",
|
||||
"Tabs": "Tabs",
|
||||
"Switch to Preview": "切回預覽頁面的時機",
|
||||
"Switch to Preview": "切回預覽頁面",
|
||||
"When Editor Blurred": "當編輯器失去焦點時",
|
||||
"When Editor Blurred, Edit On Double Click": "當編輯器失去焦點時,雙擊切換到編輯畫面",
|
||||
"On Right Click": "點選右鍵切換兩個頁面",
|
||||
"When Editor Blurred, Edit On Double Click": "當編輯器失去焦點時,點兩下開始編輯",
|
||||
"On Right Click": "點選右鍵",
|
||||
"Editor Keymap": "編輯器 Keymap",
|
||||
"default": "預設",
|
||||
"vim": "vim",
|
||||
"emacs": "emacs",
|
||||
"⚠️ Please restart boostnote after you change the keymap": "⚠️ 修改鍵盤配置請重新開啟 Boostnote ",
|
||||
"⚠️ Please restart boostnote after you change the keymap": "⚠️ 修改鍵盤配置後請重新啟動 Boostnote ",
|
||||
"Show line numbers in the editor": "在編輯器中顯示行號",
|
||||
"Allow editor to scroll past the last line": "允許編輯器捲軸捲動超過最後一行",
|
||||
"Bring in web page title when pasting URL on editor": "在編輯器貼上網址的時候,自動加上網頁標題",
|
||||
"Enable smart quotes": "啟用智慧引號",
|
||||
"Bring in web page title when pasting URL on editor": "在編輯器貼上網址時自動加上網頁標題",
|
||||
"Preview": "預覽頁面",
|
||||
"Preview Font Size": "預覽頁面字型大小",
|
||||
"Preview Font Family": "預覽頁面字體",
|
||||
"Code Block Theme": "程式碼區塊主題",
|
||||
"Allow preview to scroll past the last line": "允許預覽頁面捲軸捲動超過最後一行",
|
||||
"Show line numbers for preview code blocks": "在預覽頁面的程式碼區塊中顯示行號",
|
||||
"Show line numbers for preview code blocks": "在程式碼區塊預覽中顯示行號",
|
||||
"LaTeX Inline Open Delimiter": "LaTeX 單行開頭符號",
|
||||
"LaTeX Inline Close Delimiter": "LaTeX 單行結尾符號",
|
||||
"LaTeX Block Open Delimiter": "LaTeX 多行開頭符號",
|
||||
"LaTeX Block Close Delimiter": "LaTeX 多行結尾符號",
|
||||
"PlantUML Server": "PlantUML 伺服器",
|
||||
"Community": "社群",
|
||||
"Subscribe to Newsletter": "訂閱郵件",
|
||||
"Subscribe to Newsletter": "訂閱電子報",
|
||||
"GitHub": "GitHub",
|
||||
"Blog": "部落格",
|
||||
"Facebook Group": "Facebook 社團",
|
||||
@@ -73,34 +81,35 @@
|
||||
"About": "關於",
|
||||
"Boostnote": "Boostnote",
|
||||
"An open source note-taking app made for programmers just like you.": "一款專門為程式設計師朋友量身打造的開源筆記軟體",
|
||||
"Website": "官網",
|
||||
"Website": "官方網站",
|
||||
"Development": "開發",
|
||||
" : Development configurations for Boostnote.": " : Boostnote 的開發組態",
|
||||
" : Development configurations for Boostnote.": " : Boostnote 的開發設定",
|
||||
"Copyright (C) 2017 - 2019 BoostIO": "Copyright (C) 2017 - 2019 BoostIO",
|
||||
"License: GPL v3": "License: GPL v3",
|
||||
"Analytics": "分析",
|
||||
"Boostnote collects anonymous data for the sole purpose of improving the application, and strictly does not collect any personal information such the contents of your notes.": "Boostnote 收集匿名資料單純只為了提升軟體使用體驗,絕對不收集任何個人資料(包括筆記內容)",
|
||||
"You can see how it works on ": "你可以看看它的程式碼是如何運作 ",
|
||||
"You can choose to enable or disable this option.": "你可以選擇啟用或停用這項功能",
|
||||
"Enable analytics to help improve Boostnote": "允許數據分析以協助我們改進 Boostnote",
|
||||
"You can see how it works on ": "您可以看看它的程式碼是如何運作 ",
|
||||
"You can choose to enable or disable this option.": "您可以選擇啟用或停用這項功能",
|
||||
"Enable analytics to help improve Boostnote": "啟用數據分析以協助我們改進 Boostnote",
|
||||
"Crowdfunding": "群眾募資",
|
||||
"Dear Boostnote users,": "親愛的用戶:",
|
||||
"Thank you for using Boostnote!": "謝謝你使用 Boostnote!",
|
||||
"Dear Boostnote users,": "親愛的使用者:",
|
||||
"Thank you for using Boostnote!": "感謝您使用 Boostnote!",
|
||||
"Boostnote is used in about 200 different countries and regions by an awesome community of developers.": "大約有 200 個不同的國家和地區的優秀開發者們都在使用 Boostnote!",
|
||||
"To support our growing userbase, and satisfy community expectations,": "為了繼續支持這種發展,和滿足社群的期待,",
|
||||
"To support our growing userbase, and satisfy community expectations,": "為了繼續支持我們的使用者成長與滿足社群期待,",
|
||||
"we would like to invest more time and resources in this project.": "我們非常願意投入更多的時間和資源到這個專案中。",
|
||||
"If you use Boostnote and see its potential, help us out by supporting the project on OpenCollective!": "如果你喜歡這款軟體並且看好它的潛力, 請在 OpenCollective 上支持我們!",
|
||||
"If you use Boostnote and see its potential, help us out by supporting the project on OpenCollective!": "如果您喜歡這款軟體並且看好它的潛力, 請在 OpenCollective 上支持我們!",
|
||||
"Thanks,": "十分感謝!",
|
||||
"The Boostnote Team": "Boostnote 的維護人員",
|
||||
"The Boostnote Team": "Boostnote 的團隊",
|
||||
"Support via OpenCollective": "在 OpenCollective 上支持我們",
|
||||
"Language": "語言",
|
||||
"Default New Note": "新筆記預設類型",
|
||||
"English": "English",
|
||||
"German": "German",
|
||||
"French": "French",
|
||||
"German": "德文",
|
||||
"French": "法文",
|
||||
"Show \"Saved to Clipboard\" notification when copying": "複製的時候,顯示 \"已複製到剪貼簿\" 的通知",
|
||||
"All Notes": "所有筆記",
|
||||
"Starred": "星號收藏",
|
||||
"Are you sure to ": "你確定要 ",
|
||||
"Starred": "我的最愛",
|
||||
"Are you sure to ": "您確定要 ",
|
||||
" delete": " 刪除",
|
||||
"this folder?": "這個資料夾嗎?",
|
||||
"Confirm": "確認",
|
||||
@@ -113,13 +122,14 @@
|
||||
"Updated": "依更新時間排序",
|
||||
"Created": "依建立時間排序",
|
||||
"Alphabetically": "依字母排序",
|
||||
"Counter": "計數器",
|
||||
"Default View": "預設顯示",
|
||||
"Compressed View": "緊密顯示",
|
||||
"Search": "搜尋",
|
||||
"Blog Type": "部落格類型",
|
||||
"Blog Address": "部落格網址",
|
||||
"Save": "儲存",
|
||||
"Auth": "驗證",
|
||||
"Auth": "認證",
|
||||
"Authentication Method": "認證方法",
|
||||
"JWT": "JWT",
|
||||
"USER": "USER",
|
||||
@@ -127,47 +137,61 @@
|
||||
"Storage": "儲存空間",
|
||||
"Hotkeys": "快捷鍵",
|
||||
"Show/Hide Boostnote": "顯示/隱藏 Boostnote",
|
||||
"Toggle Editor Mode": "切換編輯器模式",
|
||||
"Delete Note": "刪除模式",
|
||||
"Restore": "還原",
|
||||
"Permanent Delete": "永久刪除",
|
||||
"Confirm note deletion": "確認刪除筆記",
|
||||
"This will permanently remove this note.": "這將會永久地刪除這條筆記",
|
||||
"Successfully applied!": "設定成功",
|
||||
"Albanian": "Albanian",
|
||||
"Chinese (zh-CN)": "简体中文",
|
||||
"Chinese (zh-TW)": "繁體中文",
|
||||
"This will permanently remove this note.": "永久地刪除此筆記",
|
||||
"Successfully applied!": "設定成功!",
|
||||
"Albanian": "阿爾巴尼亞語",
|
||||
"Chinese (zh-CN)": "简体中文 (zh-CN)",
|
||||
"Chinese (zh-TW)": "繁體中文 (zh-TW)",
|
||||
"Czech": "捷克文",
|
||||
"Danish": "Danish",
|
||||
"Japanese": "Japanese",
|
||||
"Korean": "Korean",
|
||||
"Norwegian": "Norwegian",
|
||||
"Polish": "Polish",
|
||||
"Portuguese": "Portuguese",
|
||||
"Spanish": "Spanish",
|
||||
"Unsaved Changes!": "你必須儲存一下!",
|
||||
"Russian": "Russian",
|
||||
"Danish": "丹麥文",
|
||||
"Japanese": "日文",
|
||||
"Korean": "韓文",
|
||||
"Norwegian": "挪威語",
|
||||
"Polish": "波蘭文",
|
||||
"Portuguese": "葡萄牙文",
|
||||
"Spanish": "西班牙文",
|
||||
"Unsaved Changes!": "您必須儲存一下!",
|
||||
"UserName": "使用者名稱",
|
||||
"Password": "密碼",
|
||||
"Russian": "俄羅斯語",
|
||||
"Hungarian": "匈牙利語",
|
||||
"Thai": "泰文 (ภาษาไทย)",
|
||||
"Command(⌘)": "指令(⌘)",
|
||||
"Add Storage": "新增儲存空間",
|
||||
"Name": "名稱",
|
||||
"Type": "類型",
|
||||
"File System": "檔案系統",
|
||||
"Setting up 3rd-party cloud storage integration:": "第三方雲端儲存空間設定:",
|
||||
"Cloud-Syncing-and-Backup": "雲端同步與備份",
|
||||
"Location": "位置",
|
||||
"Add": "新增",
|
||||
"Unlink Storage": "解除儲存空間連結",
|
||||
"Unlinking removes this linked storage from Boostnote. No data is removed, please manually delete the folder from your hard drive if needed.": "從 Boostnote 移除解此連結. 資料並不會被刪除,如需刪除,請手動從硬碟資料夾中刪除資料。",
|
||||
"Editor Rulers": "編輯器中顯示垂直尺規",
|
||||
"Enable": "啟用",
|
||||
"Disable": "停用",
|
||||
"Sanitization": "過濾 HTML 程式碼",
|
||||
"Only allow secure html tags (recommended)": "只允許安全的 HTML 標籤 (建議)",
|
||||
"Render newlines in Markdown paragraphs as <br>": "在 Markdown 段落中使用 <br> 換行",
|
||||
"Allow styles": "允許樣式",
|
||||
"Allow dangerous html tags": "允許危險的 HTML 標籤",
|
||||
"Convert textual arrows to beautiful signs. ⚠ This will interfere with using HTML comments in your Markdown.": "將文本箭頭轉換為完整符號。 ⚠ 注意這會影響 Markdown 的 HTML 注釋。",
|
||||
"Default New Note": "預設新筆記類型",
|
||||
"Show only related tags": "只顯示相關標籤",
|
||||
"Snippet Default Language": "程式碼片段預設語言",
|
||||
"Disable Direct Write (It will be applied after restarting)": "停用直接編輯 (重啟後生效)",
|
||||
"⚠ You have pasted a link referring an attachment that could not be found in the storage location of this note. Pasting links referring attachments is only supported if the source and destination location is the same storage. Please Drag&Drop the attachment instead! ⚠": "⚠ 您貼上了一個不存在本筆記儲存空間的附加檔案連結。貼上附加檔案連結功能只支援剪下貼上於相同儲存空間之間。請改以拖拉 Drag&Drop 附加檔案!⚠",
|
||||
"Spellcheck disabled": "拼寫檢查已關閉",
|
||||
"Save tags of a note in alphabetical order": "依照字母排序儲存標籤",
|
||||
"Enable live count of notes": "啟用即時統計筆記數量",
|
||||
"Enable smart table editor": "啟用智能表格編輯器",
|
||||
"Enable smart quotes": "啟用智能引號",
|
||||
"Allow line through checkbox": "替標示為完成的選框添加刪除線",
|
||||
"Custom CSS": "自定義 CSS",
|
||||
"Allow custom CSS for preview": "允許預覽自定義 CSS",
|
||||
"Render newlines in Markdown paragraphs as <br>": "在 Markdown 段落中使用 <br> 換行",
|
||||
"⚠ You have pasted a link referring an attachment that could not be found in the storage location of this note. Pasting links referring attachments is only supported if the source and destination location is the same storage. Please Drag&Drop the attachment instead! ⚠": "⚠ You have pasted a link referring an attachment that could not be found in the storage location of this note. Pasting links referring attachments is only supported if the source and destination location is the same storage. Please Drag&Drop the attachment instead! ⚠",
|
||||
"Spellcheck disabled": "Spellcheck disabled",
|
||||
"Show menu bar": "Show menu bar",
|
||||
"Auto Detect": "Auto Detect",
|
||||
"Filter tags/folders...": "filter tags/folders...",
|
||||
"Enable HTML label in mermaid flowcharts": "Enable HTML label in mermaid flowcharts ⚠ This option potentially has a risk of XSS.",
|
||||
"Wrap line in Snippet Note": "Wrap line in Snippet Note"
|
||||
"Snippet Default Language": "程式碼片段預設語言",
|
||||
"New notes are tagged with the filtering tags": "以過慮標籤標記新筆記",
|
||||
"Show menu bar": "顯示功能列",
|
||||
"Auto Detect": "自動偵測",
|
||||
"Filter tags/folders...": "過濾標籤/資料夾...",
|
||||
"Enable HTML label in mermaid flowcharts": "在 mermaid 流程圖中啟用 HTML 標籤 ⚠ 本選項有潛在的 XSS 安全風險。",
|
||||
"Wrap line in Snippet Note": "Snippet Note 行尾換行",
|
||||
"Enable Auto Update": "Enable Auto Update"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "boost",
|
||||
"productName": "Boostnote",
|
||||
"version": "0.15.1",
|
||||
"version": "0.16.1",
|
||||
"main": "index.js",
|
||||
"description": "Boostnote",
|
||||
"license": "GPL-3.0",
|
||||
@@ -61,6 +61,7 @@
|
||||
"chart.js": "^2.7.2",
|
||||
"codemirror": "^5.40.2",
|
||||
"codemirror-mode-elixir": "^1.1.1",
|
||||
"command-exists": "^1.2.9",
|
||||
"connected-react-router": "^6.4.0",
|
||||
"electron-config": "^1.0.0",
|
||||
"electron-gh-releases": "^2.0.4",
|
||||
@@ -79,7 +80,7 @@
|
||||
"js-yaml": "^3.13.1",
|
||||
"jsonlint-mod": "^1.7.4",
|
||||
"katex": "^0.10.1",
|
||||
"lodash": "^4.17.13",
|
||||
"lodash": "^4.17.19",
|
||||
"lodash-move": "^1.1.1",
|
||||
"markdown-it": "^6.0.1",
|
||||
"markdown-it-abbr": "^1.0.4",
|
||||
@@ -95,7 +96,7 @@
|
||||
"markdown-it-sup": "^1.0.0",
|
||||
"markdown-toc": "^1.2.0",
|
||||
"mdurl": "^1.0.1",
|
||||
"mermaid": "^8.4.2",
|
||||
"mermaid": "^8.5.2",
|
||||
"moment": "^2.10.3",
|
||||
"mousetrap": "^1.6.2",
|
||||
"mousetrap-global-bind": "^1.1.0",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 2,
|
||||
"singleQuote": true,
|
||||
"semi": false,
|
||||
"singleQuote": true
|
||||
"jsxSingleQuote": true
|
||||
}
|
||||
56
readme.md
56
readme.md
@@ -1,6 +1,26 @@
|
||||
> [We've launched desktop app of the new Boost Note now. We'll release its mobile app too in January 2020.](https://github.com/BoostIO/BoostNote.next)
|
||||
> # 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)
|
||||
|
||||

|
||||
<h1 align="center">BoostNote-Legacy</h1>
|
||||
|
||||
<h4 align="center">Note-taking app for programmers. </h4>
|
||||
<h5 align="center">Apps available for Mac, Windows and Linux.</h5>
|
||||
@@ -15,37 +35,11 @@
|
||||
|
||||
[Find the latest release of Boostnote here!](https://github.com/BoostIO/boost-releases/releases/)
|
||||
|
||||
## 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/enQtMzkxOTk4ODkyNzc0LWQxZTQwNjBlMDI4YjkyYjg2MTRiZGJhNzA1YjQ5ODA5M2M0M2NlMjI5YjhiYWQzNzgzYmU0MDMwOTlmZmZmMGE)
|
||||
- [Blog](https://medium.com/boostnote)
|
||||
- [Reddit](https://www.reddit.com/r/Boostnote/)
|
||||
|
||||
|
||||
#### More Information
|
||||
* Website: https://boostnote.io
|
||||
* [Development](https://github.com/BoostIO/Boostnote/blob/master/docs/build.md): Development configurations for Boostnote.
|
||||
* Copyright (C) 2016 - 2020 BoostIO, Inc.
|
||||
|
||||
- Website: https://boostnote.io
|
||||
- [Development](https://github.com/BoostIO/Boostnote/blob/master/docs/build.md): Development configurations for Boostnote.
|
||||
- Copyright (C) 2016 - 2020 BoostIO, Inc.
|
||||
|
||||
#### License
|
||||
|
||||
|
||||
@@ -675,6 +675,109 @@ it('should remove the all ":storage" and noteKey references', function() {
|
||||
' </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.posix.sep +
|
||||
'0.6r4zdgc22xp.png" alt="dummyImage.png" >\n' +
|
||||
' </p>\n' +
|
||||
' <p data-line="4">\n' +
|
||||
' <a href="' +
|
||||
storageFolder +
|
||||
path.posix.sep +
|
||||
'0.q2i4iw0fyx.pdf">dummyPDF.pdf</a>\n' +
|
||||
' </p>\n' +
|
||||
' <p data-line="6">\n' +
|
||||
' <img src="' +
|
||||
storageFolder +
|
||||
path.posix.sep +
|
||||
'd6c5ee92.jpg" alt="dummyImage2.jpg">\n' +
|
||||
' </p>\n' +
|
||||
' </body>\n' +
|
||||
'</html>'
|
||||
const actual = systemUnderTest.replaceStorageReferences(
|
||||
testInput,
|
||||
noteKey,
|
||||
systemUnderTest.DESTINATION_FOLDER
|
||||
)
|
||||
expect(actual).toEqual(expectedOutput)
|
||||
})
|
||||
|
||||
it('should make sure that "replaceStorageReferences" works with markdown content as well', function() {
|
||||
const noteKey = 'noteKey'
|
||||
const testInput =
|
||||
'Test input' +
|
||||
' \n' +
|
||||
'[pdf](' +
|
||||
systemUnderTest.STORAGE_FOLDER_PLACEHOLDER +
|
||||
path.posix.sep +
|
||||
noteKey +
|
||||
path.posix.sep +
|
||||
'pdf.pdf)'
|
||||
|
||||
const expectedOutput =
|
||||
'Test input' +
|
||||
' \n' +
|
||||
'[pdf](' +
|
||||
systemUnderTest.DESTINATION_FOLDER +
|
||||
path.posix.sep +
|
||||
'pdf.pdf)'
|
||||
const actual = systemUnderTest.replaceStorageReferences(
|
||||
testInput,
|
||||
noteKey,
|
||||
systemUnderTest.DESTINATION_FOLDER
|
||||
)
|
||||
expect(actual).toEqual(expectedOutput)
|
||||
})
|
||||
|
||||
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' +
|
||||
@@ -702,43 +805,45 @@ it('should remove the all ":storage" and noteKey references', function() {
|
||||
' </p>\n' +
|
||||
' </body>\n' +
|
||||
'</html>'
|
||||
const actual = systemUnderTest.removeStorageAndNoteReferences(
|
||||
const actual = systemUnderTest.replaceStorageReferences(
|
||||
testInput,
|
||||
noteKey
|
||||
noteKey,
|
||||
systemUnderTest.DESTINATION_FOLDER
|
||||
)
|
||||
expect(actual).toEqual(expectedOutput)
|
||||
})
|
||||
|
||||
it('should make sure that "removeStorageAndNoteReferences" works with markdown content as well', function() {
|
||||
it('should make sure that "replaceStorageReferences" works with markdown content as well', function() {
|
||||
const noteKey = 'noteKey'
|
||||
const testInput =
|
||||
'Test input' +
|
||||
' \n' +
|
||||
'[' +
|
||||
'image.jpg) \n' +
|
||||
'[pdf](' +
|
||||
systemUnderTest.STORAGE_FOLDER_PLACEHOLDER +
|
||||
path.posix.sep +
|
||||
noteKey +
|
||||
path.posix.sep +
|
||||
'pdf.pdf](pdf})'
|
||||
'pdf.pdf)'
|
||||
|
||||
const expectedOutput =
|
||||
'Test input' +
|
||||
' \n' +
|
||||
'[' +
|
||||
path.posix.sep +
|
||||
'image.jpg) \n' +
|
||||
'[pdf](' +
|
||||
systemUnderTest.DESTINATION_FOLDER +
|
||||
path.sep +
|
||||
'pdf.pdf](pdf})'
|
||||
const actual = systemUnderTest.removeStorageAndNoteReferences(
|
||||
path.posix.sep +
|
||||
'pdf.pdf)'
|
||||
const actual = systemUnderTest.replaceStorageReferences(
|
||||
testInput,
|
||||
noteKey
|
||||
noteKey,
|
||||
systemUnderTest.DESTINATION_FOLDER
|
||||
)
|
||||
expect(actual).toEqual(expectedOutput)
|
||||
})
|
||||
@@ -912,6 +1017,19 @@ it('should test that getAttachmentsPathAndStatus return null if noteKey, storage
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
|
||||
it('should test that getAttachmentsPathAndStatus return null if no storage found', function() {
|
||||
const noteKey = 'test'
|
||||
const storageKey = 'not_exist'
|
||||
const markdownContent = ''
|
||||
|
||||
const result = systemUnderTest.getAttachmentsPathAndStatus(
|
||||
markdownContent,
|
||||
storageKey,
|
||||
noteKey
|
||||
)
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
|
||||
it('should test that getAttachmentsPathAndStatus return the correct path and status for attachments', async function() {
|
||||
const dummyStorage = { path: 'dummyStoragePath' }
|
||||
const noteKey = 'noteKey'
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
const test = require('ava')
|
||||
const copyFile = require('browser/main/lib/dataApi/copyFile')
|
||||
|
||||
const path = require('path')
|
||||
@@ -13,23 +12,25 @@ const srcPath = path.join(srcFolder, testFile)
|
||||
const dstFolder = path.join(__dirname, '😇')
|
||||
const dstPath = path.join(dstFolder, testFile)
|
||||
|
||||
test.before(t => {
|
||||
beforeAll(() => {
|
||||
if (!fs.existsSync(srcFolder)) fs.mkdirSync(srcFolder)
|
||||
|
||||
fs.writeFileSync(srcPath, 'test')
|
||||
})
|
||||
|
||||
test('`copyFile` should handle encoded URI on src path', t => {
|
||||
it('`copyFile` should handle encoded URI on src path', done => {
|
||||
return copyFile(encodeURI(srcPath), dstPath)
|
||||
.then(() => {
|
||||
t.true(true)
|
||||
expect(true).toBe(true)
|
||||
done()
|
||||
})
|
||||
.catch(() => {
|
||||
t.true(false)
|
||||
expect(false).toBe(true)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
test.after(t => {
|
||||
afterAll(() => {
|
||||
fs.unlinkSync(srcPath)
|
||||
fs.unlinkSync(dstPath)
|
||||
execSync(removeDirCommand + '"' + srcFolder + '"')
|
||||
@@ -1,4 +1,3 @@
|
||||
const test = require('ava')
|
||||
const createFolder = require('browser/main/lib/dataApi/createFolder')
|
||||
|
||||
global.document = require('jsdom').jsdom('<body></body>')
|
||||
@@ -19,32 +18,34 @@ const CSON = require('@rokt33r/season')
|
||||
|
||||
const storagePath = path.join(os.tmpdir(), 'test/create-folder')
|
||||
|
||||
test.beforeEach(t => {
|
||||
t.context.storage = TestDummy.dummyStorage(storagePath)
|
||||
localStorage.setItem('storages', JSON.stringify([t.context.storage.cache]))
|
||||
let storageContext
|
||||
|
||||
beforeAll(() => {
|
||||
storageContext = TestDummy.dummyStorage(storagePath)
|
||||
localStorage.setItem('storages', JSON.stringify([storageContext.cache]))
|
||||
})
|
||||
|
||||
test.serial('Create a folder', t => {
|
||||
const storageKey = t.context.storage.cache.key
|
||||
it('Create a folder', done => {
|
||||
const storageKey = storageContext.cache.key
|
||||
const input = {
|
||||
name: 'created',
|
||||
color: '#ff5555'
|
||||
}
|
||||
return Promise.resolve()
|
||||
.then(function doTest() {
|
||||
.then(() => {
|
||||
return createFolder(storageKey, input)
|
||||
})
|
||||
.then(function assert(data) {
|
||||
t.true(_.find(data.storage.folders, input) != null)
|
||||
.then(data => {
|
||||
expect(_.find(data.storage.folders, input)).not.toBeNull()
|
||||
const jsonData = CSON.readFileSync(
|
||||
path.join(data.storage.path, 'boostnote.json')
|
||||
)
|
||||
console.log(path.join(data.storage.path, 'boostnote.json'))
|
||||
t.true(_.find(jsonData.folders, input) != null)
|
||||
expect(_.find(jsonData.folders, input)).not.toBeNull()
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
test.after(function after() {
|
||||
afterAll(() => {
|
||||
localStorage.clear()
|
||||
sander.rimrafSync(storagePath)
|
||||
})
|
||||
@@ -1,4 +1,3 @@
|
||||
const test = require('ava')
|
||||
const createNote = require('browser/main/lib/dataApi/createNote')
|
||||
|
||||
global.document = require('jsdom').jsdom('<body></body>')
|
||||
@@ -19,14 +18,16 @@ const faker = require('faker')
|
||||
|
||||
const storagePath = path.join(os.tmpdir(), 'test/create-note')
|
||||
|
||||
test.beforeEach(t => {
|
||||
t.context.storage = TestDummy.dummyStorage(storagePath)
|
||||
localStorage.setItem('storages', JSON.stringify([t.context.storage.cache]))
|
||||
let storageContext
|
||||
|
||||
beforeEach(() => {
|
||||
storageContext = TestDummy.dummyStorage(storagePath)
|
||||
localStorage.setItem('storages', JSON.stringify([storageContext.cache]))
|
||||
})
|
||||
|
||||
test.serial('Create a note', t => {
|
||||
const storageKey = t.context.storage.cache.key
|
||||
const folderKey = t.context.storage.json.folders[0].key
|
||||
it('Create a note', done => {
|
||||
const storageKey = storageContext.cache.key
|
||||
const folderKey = storageContext.json.folders[0].key
|
||||
|
||||
const randLinesHighlightedArray = new Array(10)
|
||||
.fill()
|
||||
@@ -58,58 +59,58 @@ test.serial('Create a note', t => {
|
||||
input2.title = input2.content.split('\n').shift()
|
||||
|
||||
return Promise.resolve()
|
||||
.then(function doTest() {
|
||||
.then(() => {
|
||||
return Promise.all([
|
||||
createNote(storageKey, input1),
|
||||
createNote(storageKey, input2)
|
||||
])
|
||||
})
|
||||
.then(function assert(data) {
|
||||
.then(data => {
|
||||
const data1 = data[0]
|
||||
const data2 = data[1]
|
||||
|
||||
t.is(storageKey, data1.storage)
|
||||
expect(storageKey).toEqual(data1.storage)
|
||||
const jsonData1 = CSON.readFileSync(
|
||||
path.join(storagePath, 'notes', data1.key + '.cson')
|
||||
)
|
||||
|
||||
t.is(input1.title, data1.title)
|
||||
t.is(input1.title, jsonData1.title)
|
||||
t.is(input1.description, data1.description)
|
||||
t.is(input1.description, jsonData1.description)
|
||||
t.is(input1.tags.length, data1.tags.length)
|
||||
t.is(input1.tags.length, jsonData1.tags.length)
|
||||
t.is(input1.snippets.length, data1.snippets.length)
|
||||
t.is(input1.snippets.length, jsonData1.snippets.length)
|
||||
t.is(input1.snippets[0].content, data1.snippets[0].content)
|
||||
t.is(input1.snippets[0].content, jsonData1.snippets[0].content)
|
||||
t.is(input1.snippets[0].name, data1.snippets[0].name)
|
||||
t.is(input1.snippets[0].name, jsonData1.snippets[0].name)
|
||||
t.deepEqual(
|
||||
input1.snippets[0].linesHighlighted,
|
||||
expect(input1.title).toEqual(data1.title)
|
||||
expect(input1.title).toEqual(jsonData1.title)
|
||||
expect(input1.description).toEqual(data1.description)
|
||||
expect(input1.description).toEqual(jsonData1.description)
|
||||
expect(input1.tags.length).toEqual(data1.tags.length)
|
||||
expect(input1.tags.length).toEqual(jsonData1.tags.length)
|
||||
expect(input1.snippets.length).toEqual(data1.snippets.length)
|
||||
expect(input1.snippets.length).toEqual(jsonData1.snippets.length)
|
||||
expect(input1.snippets[0].content).toEqual(data1.snippets[0].content)
|
||||
expect(input1.snippets[0].content).toEqual(jsonData1.snippets[0].content)
|
||||
expect(input1.snippets[0].name).toEqual(data1.snippets[0].name)
|
||||
expect(input1.snippets[0].name).toEqual(jsonData1.snippets[0].name)
|
||||
expect(input1.snippets[0].linesHighlighted).toEqual(
|
||||
data1.snippets[0].linesHighlighted
|
||||
)
|
||||
t.deepEqual(
|
||||
input1.snippets[0].linesHighlighted,
|
||||
expect(input1.snippets[0].linesHighlighted).toEqual(
|
||||
jsonData1.snippets[0].linesHighlighted
|
||||
)
|
||||
|
||||
t.is(storageKey, data2.storage)
|
||||
expect(storageKey).toEqual(data2.storage)
|
||||
const jsonData2 = CSON.readFileSync(
|
||||
path.join(storagePath, 'notes', data2.key + '.cson')
|
||||
)
|
||||
t.is(input2.title, data2.title)
|
||||
t.is(input2.title, jsonData2.title)
|
||||
t.is(input2.content, data2.content)
|
||||
t.is(input2.content, jsonData2.content)
|
||||
t.is(input2.tags.length, data2.tags.length)
|
||||
t.is(input2.tags.length, jsonData2.tags.length)
|
||||
t.deepEqual(input2.linesHighlighted, data2.linesHighlighted)
|
||||
t.deepEqual(input2.linesHighlighted, jsonData2.linesHighlighted)
|
||||
expect(input2.title).toEqual(data2.title)
|
||||
expect(input2.title).toEqual(jsonData2.title)
|
||||
expect(input2.content).toEqual(data2.content)
|
||||
expect(input2.content).toEqual(jsonData2.content)
|
||||
expect(input2.tags.length).toEqual(data2.tags.length)
|
||||
expect(input2.tags.length).toEqual(jsonData2.tags.length)
|
||||
expect(input2.linesHighlighted).toEqual(data2.linesHighlighted)
|
||||
expect(input2.linesHighlighted).toEqual(jsonData2.linesHighlighted)
|
||||
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
test.after(function after() {
|
||||
afterAll(function after() {
|
||||
localStorage.clear()
|
||||
sander.rimrafSync(storagePath)
|
||||
})
|
||||
@@ -1,4 +1,3 @@
|
||||
const test = require('ava')
|
||||
const createNoteFromUrl = require('browser/main/lib/dataApi/createNoteFromUrl')
|
||||
|
||||
global.document = require('jsdom').jsdom('<body></body>')
|
||||
@@ -18,32 +17,34 @@ const CSON = require('@rokt33r/season')
|
||||
|
||||
const storagePath = path.join(os.tmpdir(), 'test/create-note-from-url')
|
||||
|
||||
test.beforeEach(t => {
|
||||
t.context.storage = TestDummy.dummyStorage(storagePath)
|
||||
localStorage.setItem('storages', JSON.stringify([t.context.storage.cache]))
|
||||
let storageContext
|
||||
|
||||
beforeEach(() => {
|
||||
storageContext = TestDummy.dummyStorage(storagePath)
|
||||
localStorage.setItem('storages', JSON.stringify([storageContext.cache]))
|
||||
})
|
||||
|
||||
test.serial('Create a note from URL', t => {
|
||||
const storageKey = t.context.storage.cache.key
|
||||
const folderKey = t.context.storage.json.folders[0].key
|
||||
it('Create a note from URL', () => {
|
||||
const storageKey = storageContext.cache.key
|
||||
const folderKey = storageContext.json.folders[0].key
|
||||
|
||||
const url = 'https://shapeshed.com/writing-cross-platform-node/'
|
||||
|
||||
return createNoteFromUrl(url, storageKey, folderKey).then(function assert({
|
||||
note
|
||||
}) {
|
||||
t.is(storageKey, note.storage)
|
||||
expect(storageKey).toEqual(note.storage)
|
||||
const jsonData = CSON.readFileSync(
|
||||
path.join(storagePath, 'notes', note.key + '.cson')
|
||||
)
|
||||
|
||||
// Test if saved content is matching the created in memory note
|
||||
t.is(note.content, jsonData.content)
|
||||
t.is(note.tags.length, jsonData.tags.length)
|
||||
expect(note.content).toEqual(jsonData.content)
|
||||
expect(note.tags.length).toEqual(jsonData.tags.length)
|
||||
})
|
||||
})
|
||||
|
||||
test.after(function after() {
|
||||
afterAll(function after() {
|
||||
localStorage.clear()
|
||||
sander.rimrafSync(storagePath)
|
||||
})
|
||||
@@ -1,4 +1,3 @@
|
||||
const test = require('ava')
|
||||
const createSnippet = require('browser/main/lib/dataApi/createSnippet')
|
||||
const sander = require('sander')
|
||||
const os = require('os')
|
||||
@@ -7,29 +6,27 @@ const path = require('path')
|
||||
const snippetFilePath = path.join(os.tmpdir(), 'test', 'create-snippet')
|
||||
const snippetFile = path.join(snippetFilePath, 'snippets.json')
|
||||
|
||||
test.beforeEach(t => {
|
||||
beforeEach(() => {
|
||||
sander.writeFileSync(snippetFile, '[]')
|
||||
})
|
||||
|
||||
test.serial('Create a snippet', t => {
|
||||
it('Create a snippet', () => {
|
||||
return Promise.resolve()
|
||||
.then(function doTest() {
|
||||
return Promise.all([createSnippet(snippetFile)])
|
||||
})
|
||||
.then(() => Promise.all([createSnippet(snippetFile)]))
|
||||
.then(function assert(data) {
|
||||
data = data[0]
|
||||
const snippets = JSON.parse(sander.readFileSync(snippetFile))
|
||||
const snippet = snippets.find(
|
||||
currentSnippet => currentSnippet.id === data.id
|
||||
)
|
||||
t.not(snippet, undefined)
|
||||
t.is(snippet.name, data.name)
|
||||
t.deepEqual(snippet.prefix, data.prefix)
|
||||
t.is(snippet.content, data.content)
|
||||
t.deepEqual(snippet.linesHighlighted, data.linesHighlighted)
|
||||
expect(snippet).not.toBeUndefined()
|
||||
expect(snippet.name).toEqual(data.name)
|
||||
expect(snippet.prefix).toEqual(data.prefix)
|
||||
expect(snippet.content).toEqual(data.content)
|
||||
expect(snippet.linesHighlighted).toEqual(data.linesHighlighted)
|
||||
})
|
||||
})
|
||||
|
||||
test.after.always(() => {
|
||||
afterAll(() => {
|
||||
sander.rimrafSync(snippetFilePath)
|
||||
})
|
||||
@@ -1,4 +1,3 @@
|
||||
const test = require('ava')
|
||||
const deleteFolder = require('browser/main/lib/dataApi/deleteFolder')
|
||||
const attachmentManagement = require('browser/main/lib/dataApi/attachmentManagement')
|
||||
const createNote = require('browser/main/lib/dataApi/createNote')
|
||||
@@ -23,14 +22,16 @@ const CSON = require('@rokt33r/season')
|
||||
|
||||
const storagePath = path.join(os.tmpdir(), 'test/delete-folder')
|
||||
|
||||
test.beforeEach(t => {
|
||||
t.context.storage = TestDummy.dummyStorage(storagePath)
|
||||
localStorage.setItem('storages', JSON.stringify([t.context.storage.cache]))
|
||||
let storageContext
|
||||
|
||||
beforeEach(() => {
|
||||
storageContext = TestDummy.dummyStorage(storagePath)
|
||||
localStorage.setItem('storages', JSON.stringify([storageContext.cache]))
|
||||
})
|
||||
|
||||
test.serial('Delete a folder', t => {
|
||||
const storageKey = t.context.storage.cache.key
|
||||
const folderKey = t.context.storage.json.folders[0].key
|
||||
it('Delete a folder', () => {
|
||||
const storageKey = storageContext.cache.key
|
||||
const folderKey = storageContext.json.folders[0].key
|
||||
let noteKey
|
||||
|
||||
const input1 = {
|
||||
@@ -72,16 +73,15 @@ test.serial('Delete a folder', t => {
|
||||
return deleteFolder(storageKey, folderKey)
|
||||
})
|
||||
.then(function assert(data) {
|
||||
t.true(_.find(data.storage.folders, { key: folderKey }) == null)
|
||||
expect(_.find(data.storage.folders, { key: folderKey })).toBeUndefined()
|
||||
const jsonData = CSON.readFileSync(
|
||||
path.join(data.storage.path, 'boostnote.json')
|
||||
)
|
||||
|
||||
t.true(_.find(jsonData.folders, { key: folderKey }) == null)
|
||||
expect(_.find(jsonData.folders, { key: folderKey })).toBeUndefined()
|
||||
const notePaths = sander.readdirSync(data.storage.path, 'notes')
|
||||
t.is(
|
||||
notePaths.length,
|
||||
t.context.storage.notes.filter(note => note.folder !== folderKey).length
|
||||
expect(notePaths.length).toBe(
|
||||
storageContext.notes.filter(note => note.folder !== folderKey).length
|
||||
)
|
||||
|
||||
const attachmentFolderPath = path.join(
|
||||
@@ -89,11 +89,11 @@ test.serial('Delete a folder', t => {
|
||||
attachmentManagement.DESTINATION_FOLDER,
|
||||
noteKey
|
||||
)
|
||||
t.false(fs.existsSync(attachmentFolderPath))
|
||||
expect(fs.existsSync(attachmentFolderPath)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
test.after.always(function after() {
|
||||
afterAll(() => {
|
||||
localStorage.clear()
|
||||
sander.rimrafSync(storagePath)
|
||||
})
|
||||
@@ -52,12 +52,20 @@ test.serial('Export a folder', t => {
|
||||
}
|
||||
input2.title = 'input2'
|
||||
|
||||
const config = {
|
||||
export: {
|
||||
metadata: 'DONT_EXPORT',
|
||||
variable: 'boostnote',
|
||||
prefixAttachmentFolder: false
|
||||
}
|
||||
}
|
||||
|
||||
return createNote(storageKey, input1)
|
||||
.then(function() {
|
||||
return createNote(storageKey, input2)
|
||||
})
|
||||
.then(function() {
|
||||
return exportFolder(storageKey, folderKey, 'md', storagePath)
|
||||
return exportFolder(storageKey, folderKey, 'md', storagePath, config)
|
||||
})
|
||||
.then(function assert() {
|
||||
let filePath = path.join(storagePath, 'input1.md')
|
||||
|
||||
@@ -35,7 +35,16 @@ test.serial('Export a storage', t => {
|
||||
acc[folder.key] = folder.name
|
||||
return acc
|
||||
}, {})
|
||||
return exportStorage(storageKey, 'md', exportDir).then(() => {
|
||||
|
||||
const config = {
|
||||
export: {
|
||||
metadata: 'DONT_EXPORT',
|
||||
variable: 'boostnote',
|
||||
prefixAttachmentFolder: false
|
||||
}
|
||||
}
|
||||
|
||||
return exportStorage(storageKey, 'md', exportDir, config).then(() => {
|
||||
notes.forEach(note => {
|
||||
const noteDir = path.join(
|
||||
exportDir,
|
||||
|
||||
189
tests/lib/__snapshots__/markdown.test.js.snap
Normal file
189
tests/lib/__snapshots__/markdown.test.js.snap
Normal file
@@ -0,0 +1,189 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Markdown.render() should render PlantUML Ditaa correctly 1`] = `
|
||||
"<img src=\\"http://www.plantuml.com/plantuml/png/SoWkIImgISaiIKpaqjQ50cq51GLj93Q2mrMZ00NQO3cmHX3RJW4cKmDI4v9QKQ805a8nfyObCp6zA34NgCObFxiqDpMl1AIcHj4tCJqpLH5i18evG52TKbk3B8og1kmC0cvMKB1Im0NYkA2ckMRcANWabgQbvYau5YMbPfP0p4UOWmcqkHnIyrB0GG00\\" alt=\\"uml diagram\\" />
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`Markdown.render() should render PlantUML Gantt correctly 1`] = `
|
||||
"<img src=\\"http://www.plantuml.com/plantuml/svg/SoWkIImgIK_CAodXYWueoY_9BwaiI5L8IItEJC-BLSX9B2ufLZ0qLKX9h2pcYWv9BIvHA82fWaiRu906crsia5YYW6cqUh52QbuAbmEG0DiE0000\\" alt=\\"uml diagram\\" />
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`Markdown.render() should render PlantUML MindMaps correctly 1`] = `
|
||||
"<img src=\\"http://www.plantuml.com/plantuml/svg/JOzD3e8m44Rtd6BMtNW192IM5I29HEDsAbKdeLD2MvNRIsjCMCsRlFd9LpgFipV4Wy4f4o2r8kHC23Yhm3wi9A0X3XzeYNrgwx1H6wvb1KTjqtRJoYhMtexBSAqJUescwoEUq4tn3xp9Fm7XfUS5HiiFO3Gw7SjT4QUCkkKxLy2-WAvl3rkrtEclBdOCXcnMwZN7ByiN\\" alt=\\"uml diagram\\" />
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`Markdown.render() should render PlantUML Umls correctly 1`] = `
|
||||
"<img src=\\"http://www.plantuml.com/plantuml/svg/LOzD2eCm44RtESMtj0jx01V5E_G4Gvngo2_912gbTsz4LBfylCV7p5Y4ibJlbEENG2AocHV1P39hCJ6eOar8bCaZaROqyrDMnzWqXTcn8YqnGzSYqNC-q76sweoW5zOsLi57uMpHz-WESslY0jmVw1AjdaE30IPeLoVUceLTslrL3-2tS9ZA_qZRtm_vgh7PzkOF\\" alt=\\"uml diagram\\" />
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`Markdown.render() should render PlantUML WBS correctly 1`] = `
|
||||
"<img src=\\"http://www.plantuml.com/plantuml/svg/ZP2_JiD03CRtFeNdRF04fR140gdGeREv-z8plVYYimFYxSabKbaxsR9-ylTdRyxLVpvjrz5XDb6OqR6MqEPRYSXPz4BdmsdNTVJAiuP4da1JBLy8lbmxUYxZbE6Wa_CLgUI8IXymS0rf9NeL5yxKDt24EhiKfMDcRNzVO79HcX8RLdvLfZBGa_KtFx2RKcpK7TZ3dTpZfWgskMAZ9jIXr94rW4PubM1RbBZOb-6NtcS9LpgBjlj_1w9QldbPjZHxQ5pg_GC0\\" alt=\\"uml diagram\\" />
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`Markdown.render() should render footnote correctly 1`] = `
|
||||
"<p data-line=\\"1\\"><sup class=\\"footnote-ref\\"><a href=\\"#fn1\\" id=\\"fnref1\\">[1]</a></sup><br />
|
||||
hello-world: <a href=\\"https://github.com/BoostIO/Boostnote/\\">https://github.com/BoostIO/Boostnote/</a></p>
|
||||
<hr class=\\"footnotes-sep\\" />
|
||||
<section class=\\"footnotes\\">
|
||||
<ol class=\\"footnotes-list\\">
|
||||
<li id=\\"fn1\\" class=\\"footnote-item\\"><p>hello-world <a href=\\"#fnref1\\" class=\\"footnote-backref\\">↩︎</a></p>
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`Markdown.render() should render line breaks correctly 1`] = `
|
||||
"<p data-line=\\"0\\">This is the first line.<br />
|
||||
This is the second line.</p>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`Markdown.render() should render line breaks correctly 2`] = `
|
||||
"<p data-line=\\"0\\">This is the first line.
|
||||
This is the second line.</p>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`Markdown.render() should render shortcuts correctly 1`] = `
|
||||
"<p data-line=\\"0\\"><kbd>Ctrl</kbd></p>
|
||||
<p data-line=\\"2\\"><kbd>Ctrl</kbd></p>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`Markdown.render() should renders [TOC] placholder correctly 1`] = `
|
||||
"<p data-line=\\"1\\"><div class=\\"markdownIt-TOC-wrapper\\"><ul class=\\"markdownIt-TOC\\">
|
||||
<li><a href=\\"#H1\\">H1</a>
|
||||
<ul>
|
||||
<li><a href=\\"#H2\\">H2</a>
|
||||
<ul>
|
||||
<li><a href=\\"#H3\\">H3</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div></p>
|
||||
<h1 id=\\"H1\\" data-line=\\"2\\">H1</h1>
|
||||
<h2 id=\\"H2\\" data-line=\\"3\\">H2</h2>
|
||||
<h3 id=\\"H3\\" data-line=\\"4\\">H3</h3>
|
||||
<p data-line=\\"5\\">###$ H4</p>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`Markdown.render() should renders KaTeX correctly 1`] = `
|
||||
"<span class=\\"katex-display\\"><span class=\\"katex\\"><span class=\\"katex-mathml\\"><math><semantics><mrow><mi>c</mi><mo>=</mo><mi>p</mi><mi>m</mi><mi>s</mi><mi>q</mi><mi>r</mi><mi>t</mi><mrow><msup><mi>a</mi><mn>2</mn></msup><mo>+</mo><msup><mi>b</mi><mn>2</mn></msup></mrow></mrow><annotation encoding=\\"application/x-tex\\">c = pmsqrt{a^2 + b^2}</annotation></semantics></math></span><span class=\\"katex-html\\" aria-hidden=\\"true\\"><span class=\\"base\\"><span class=\\"strut\\" style=\\"height:0.43056em;vertical-align:0em;\\"></span><span class=\\"mord mathdefault\\">c</span><span class=\\"mspace\\" style=\\"margin-right:0.2777777777777778em;\\"></span><span class=\\"mrel\\">=</span><span class=\\"mspace\\" style=\\"margin-right:0.2777777777777778em;\\"></span></span><span class=\\"base\\"><span class=\\"strut\\" style=\\"height:1.0585479999999998em;vertical-align:-0.19444em;\\"></span><span class=\\"mord mathdefault\\">p</span><span class=\\"mord mathdefault\\">m</span><span class=\\"mord mathdefault\\">s</span><span class=\\"mord mathdefault\\" style=\\"margin-right:0.03588em;\\">q</span><span class=\\"mord mathdefault\\" style=\\"margin-right:0.02778em;\\">r</span><span class=\\"mord mathdefault\\">t</span><span class=\\"mord\\"><span class=\\"mord\\"><span class=\\"mord mathdefault\\">a</span><span class=\\"msupsub\\"><span class=\\"vlist-t\\"><span class=\\"vlist-r\\"><span class=\\"vlist\\" style=\\"height:0.8641079999999999em;\\"><span style=\\"top:-3.113em;margin-right:0.05em;\\"><span class=\\"pstrut\\" style=\\"height:2.7em;\\"></span><span class=\\"sizing reset-size6 size3 mtight\\"><span class=\\"mord mtight\\">2</span></span></span></span></span></span></span></span><span class=\\"mspace\\" style=\\"margin-right:0.2222222222222222em;\\"></span><span class=\\"mbin\\">+</span><span class=\\"mspace\\" style=\\"margin-right:0.2222222222222222em;\\"></span><span class=\\"mord\\"><span class=\\"mord mathdefault\\">b</span><span class=\\"msupsub\\"><span class=\\"vlist-t\\"><span class=\\"vlist-r\\"><span class=\\"vlist\\" style=\\"height:0.8641079999999999em;\\"><span style=\\"top:-3.113em;margin-right:0.05em;\\"><span class=\\"pstrut\\" style=\\"height:2.7em;\\"></span><span class=\\"sizing reset-size6 size3 mtight\\"><span class=\\"mord mtight\\">2</span></span></span></span></span></span></span></span></span></span></span></span></span>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`Markdown.render() should renders abbrevations correctly 1`] = `
|
||||
"<h2 id=\\"abbr\\" data-line=\\"1\\">abbr</h2>
|
||||
<p data-line=\\"3\\">The <abbr title=\\"Hyper Text Markup Language\\">HTML</abbr> specification<br />
|
||||
is maintained by the <abbr title=\\"World Wide Web Consortium\\">W3C</abbr>.</p>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`Markdown.render() should renders checkboxes 1`] = `
|
||||
"<ul>
|
||||
<li class=\\"taskListItem\\" data-line=\\"1\\"><input type=\\"checkbox\\" id=\\"checkbox-2\\" /> Unchecked</li>
|
||||
<li class=\\"taskListItem checked\\" data-line=\\"2\\"><input type=\\"checkbox\\" checked id=\\"checkbox-3\\" /> Checked</li>
|
||||
</ul>
|
||||
"
|
||||
`;
|
||||
|
||||
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';
|
||||
</code>
|
||||
</pre>"
|
||||
`;
|
||||
|
||||
exports[`Markdown.render() should renders definition lists correctly 1`] = `
|
||||
"<h2 id=\\"definition-list\\" data-line=\\"1\\">definition list</h2>
|
||||
<h3 id=\\"list-1\\" data-line=\\"3\\">list 1</h3>
|
||||
<dl>
|
||||
<dt data-line=\\"5\\">Term 1</dt>
|
||||
<dd data-line=\\"6\\">Definition 1</dd>
|
||||
<dt data-line=\\"8\\">Term 2</dt>
|
||||
<dd data-line=\\"9\\">Definition 2a</dd>
|
||||
<dd data-line=\\"10\\">Definition 2b</dd>
|
||||
</dl>
|
||||
<p data-line=\\"12\\">Term 3<br />
|
||||
~</p>
|
||||
<h3 id=\\"list-2\\" data-line=\\"16\\">list 2</h3>
|
||||
<dl>
|
||||
<dt data-line=\\"18\\">Term 1</dt>
|
||||
<dd data-line=\\"20\\">
|
||||
<p data-line=\\"20\\">Definition 1</p>
|
||||
</dd>
|
||||
<dt data-line=\\"22\\">Term 2 with <em>inline markup</em></dt>
|
||||
<dd data-line=\\"24\\">
|
||||
<p data-line=\\"24\\">Definition 2</p>
|
||||
<pre><code> { some code, part of Definition 2 }
|
||||
</code></pre>
|
||||
<p data-line=\\"28\\">Third paragraph of definition 2.</p>
|
||||
</dd>
|
||||
</dl>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`Markdown.render() should renders markdown correctly 1`] = `
|
||||
"<h1 id=\\"Welcome-to-Boostnote\\" data-line=\\"1\\">Welcome to Boostnote!</h1>
|
||||
<h2 id=\\"Click-here-to-edit-markdown\\" data-line=\\"2\\">Click here to edit markdown 👋</h2>
|
||||
<iframe width=\\"560\\" height=\\"315\\" src=\\"https://www.youtube.com/embed/L0qNPLsvmyM\\" frameborder=\\"0\\" allowfullscreen></iframe>
|
||||
<h2 id=\\"Docs\\" data-line=\\"6\\">Docs 📝</h2>
|
||||
<ul>
|
||||
<li data-line=\\"7\\"><a href=\\"https://hackernoon.com/boostnote-boost-your-happiness-productivity-and-creativity-315034efeebe\\">Boostnote | Boost your happiness, productivity and creativity.</a></li>
|
||||
<li data-line=\\"8\\"><a href=\\"https://github.com/BoostIO/Boostnote/wiki/Cloud-Syncing-and-Backup\\">Cloud Syncing & Backups</a></li>
|
||||
<li data-line=\\"9\\"><a href=\\"https://github.com/BoostIO/Boostnote/wiki/Sync-Data-Across-Desktop-and-Mobile-apps\\">How to sync your data across Desktop and Mobile apps</a></li>
|
||||
<li data-line=\\"10\\"><a href=\\"https://github.com/BoostIO/Boostnote/wiki/Evernote\\">Convert data from <strong>Evernote</strong> to Boostnote.</a></li>
|
||||
<li data-line=\\"11\\"><a href=\\"https://github.com/BoostIO/Boostnote/wiki/Keyboard-Shortcuts\\">Keyboard Shortcuts</a></li>
|
||||
<li data-line=\\"12\\"><a href=\\"https://github.com/BoostIO/Boostnote/wiki/Keymaps-in-Editor-mode\\">Keymaps in Editor mode</a></li>
|
||||
<li data-line=\\"13\\"><a href=\\"https://github.com/BoostIO/Boostnote/wiki/Syntax-Highlighting\\">How to set syntax highlight in Snippet note</a></li>
|
||||
</ul>
|
||||
<hr />
|
||||
<h2 id=\\"Article-Archive\\" data-line=\\"17\\">Article Archive 📚</h2>
|
||||
<ul>
|
||||
<li data-line=\\"18\\"><a href=\\"http://bit.ly/2mOJPu7\\">Reddit English</a></li>
|
||||
<li data-line=\\"19\\"><a href=\\"https://www.reddit.com/r/boostnote_es/\\">Reddit Spanish</a></li>
|
||||
<li data-line=\\"20\\"><a href=\\"https://www.reddit.com/r/boostnote_cn/\\">Reddit Chinese</a></li>
|
||||
<li data-line=\\"21\\"><a href=\\"https://www.reddit.com/r/boostnote_jp/\\">Reddit Japanese</a></li>
|
||||
</ul>
|
||||
<hr />
|
||||
<h2 id=\\"Community\\" data-line=\\"25\\">Community 🍻</h2>
|
||||
<ul>
|
||||
<li data-line=\\"26\\"><a href=\\"http://bit.ly/2AWWzkD\\">GitHub</a></li>
|
||||
<li data-line=\\"27\\"><a href=\\"http://bit.ly/2z8BUJZ\\">Twitter</a></li>
|
||||
<li data-line=\\"28\\"><a href=\\"http://bit.ly/2jcca8t\\">Facebook Group</a></li>
|
||||
</ul>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`Markdown.render() should renders sub correctly 1`] = `
|
||||
"<h2 id=\\"sub\\" data-line=\\"1\\">sub</h2>
|
||||
<p data-line=\\"3\\">H<sub>2</sub>0</p>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`Markdown.render() should renders sup correctly 1`] = `
|
||||
"<h2 id=\\"sup\\" data-line=\\"1\\">sup</h2>
|
||||
<p data-line=\\"3\\">29<sup>th</sup></p>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`Markdown.render() should text with quotes correctly 1`] = `
|
||||
"<p data-line=\\"0\\">This is a “QUOTE”.</p>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`Markdown.render() should text with quotes correctly 2`] = `
|
||||
"<p data-line=\\"0\\">This is a "QUOTE".</p>
|
||||
"
|
||||
`;
|
||||
@@ -1,46 +1,45 @@
|
||||
const { escapeHtmlCharacters } = require('browser/lib/utils')
|
||||
const test = require('ava')
|
||||
|
||||
test('escapeHtmlCharacters should return the original string if nothing needed to escape', t => {
|
||||
test('escapeHtmlCharacters should return the original string if nothing needed to escape', () => {
|
||||
const input = 'Nothing to be escaped'
|
||||
const expected = 'Nothing to be escaped'
|
||||
const actual = escapeHtmlCharacters(input)
|
||||
t.is(actual, expected)
|
||||
expect(actual).toBe(expected)
|
||||
})
|
||||
|
||||
test('escapeHtmlCharacters should skip code block if that option is enabled', t => {
|
||||
test('escapeHtmlCharacters should skip code block if that option is enabled', () => {
|
||||
const input = ` <no escape>
|
||||
<escapeMe>`
|
||||
const expected = ` <no escape>
|
||||
<escapeMe>`
|
||||
const actual = escapeHtmlCharacters(input, { detectCodeBlock: true })
|
||||
t.is(actual, expected)
|
||||
expect(actual).toBe(expected)
|
||||
})
|
||||
|
||||
test('escapeHtmlCharacters should NOT skip character not in code block but start with 4 spaces', t => {
|
||||
test('escapeHtmlCharacters should NOT skip character not in code block but start with 4 spaces', () => {
|
||||
const input = '4 spaces &'
|
||||
const expected = '4 spaces &'
|
||||
const actual = escapeHtmlCharacters(input, { detectCodeBlock: true })
|
||||
t.is(actual, expected)
|
||||
expect(actual).toBe(expected)
|
||||
})
|
||||
|
||||
test('escapeHtmlCharacters should NOT skip code block if that option is NOT enabled', t => {
|
||||
test('escapeHtmlCharacters should NOT skip code block if that option is NOT enabled', () => {
|
||||
const input = ` <no escape>
|
||||
<escapeMe>`
|
||||
const expected = ` <no escape>
|
||||
<escapeMe>`
|
||||
const actual = escapeHtmlCharacters(input)
|
||||
t.is(actual, expected)
|
||||
expect(actual).toBe(expected)
|
||||
})
|
||||
|
||||
test("escapeHtmlCharacters should NOT escape & character if it's a part of an escaped character", t => {
|
||||
test("escapeHtmlCharacters should NOT escape & character if it's a part of an escaped character", () => {
|
||||
const input = 'Do not escape & or " but do escape &'
|
||||
const expected = 'Do not escape & or " but do escape &'
|
||||
const actual = escapeHtmlCharacters(input)
|
||||
t.is(actual, expected)
|
||||
expect(actual).toBe(expected)
|
||||
})
|
||||
|
||||
test('escapeHtmlCharacters should skip char if in code block', t => {
|
||||
test('escapeHtmlCharacters should skip char if in code block', () => {
|
||||
const input = `
|
||||
\`\`\`
|
||||
<dontescapeme>
|
||||
@@ -62,12 +61,12 @@ dasdasdasd
|
||||
\`\`\`
|
||||
`
|
||||
const actual = escapeHtmlCharacters(input, { detectCodeBlock: true })
|
||||
t.is(actual, expected)
|
||||
expect(actual).toBe(expected)
|
||||
})
|
||||
|
||||
test('escapeHtmlCharacters should return the correct result', t => {
|
||||
test('escapeHtmlCharacters should return the correct result', () => {
|
||||
const input = '& < > " \''
|
||||
const expected = '& < > " ''
|
||||
const actual = escapeHtmlCharacters(input)
|
||||
t.is(actual, expected)
|
||||
expect(actual).toBe(expected)
|
||||
})
|
||||
@@ -1,4 +1,3 @@
|
||||
const test = require('ava')
|
||||
const { findStorage } = require('browser/lib/findStorage')
|
||||
|
||||
global.document = require('jsdom').jsdom('<body></body>')
|
||||
@@ -16,20 +15,22 @@ const sander = require('sander')
|
||||
const os = require('os')
|
||||
const storagePath = path.join(os.tmpdir(), 'test/find-storage')
|
||||
|
||||
test.beforeEach(t => {
|
||||
t.context.storage = TestDummy.dummyStorage(storagePath)
|
||||
localStorage.setItem('storages', JSON.stringify([t.context.storage.cache]))
|
||||
let storageContext
|
||||
|
||||
beforeEach(() => {
|
||||
storageContext = TestDummy.dummyStorage(storagePath)
|
||||
localStorage.setItem('storages', JSON.stringify([storageContext.cache]))
|
||||
})
|
||||
|
||||
// Unit test
|
||||
test('findStorage() should return a correct storage path(string)', t => {
|
||||
const storageKey = t.context.storage.cache.key
|
||||
test('findStorage() should return a correct storage path(string)', () => {
|
||||
const storageKey = storageContext.cache.key
|
||||
|
||||
t.is(findStorage(storageKey).key, storageKey)
|
||||
t.is(findStorage(storageKey).path, storagePath)
|
||||
expect(findStorage(storageKey).key).toBe(storageKey)
|
||||
expect(findStorage(storageKey).path).toBe(storagePath)
|
||||
})
|
||||
|
||||
test.after(function after() {
|
||||
afterAll(function after() {
|
||||
localStorage.clear()
|
||||
sander.rimrafSync(storagePath)
|
||||
})
|
||||
@@ -2,11 +2,10 @@
|
||||
* @fileoverview Unit test for browser/lib/findTitle
|
||||
*/
|
||||
|
||||
const test = require('ava')
|
||||
const { findNoteTitle } = require('browser/lib/findNoteTitle')
|
||||
|
||||
// Unit test
|
||||
test('findNoteTitle#find should return a correct title (string)', t => {
|
||||
test('findNoteTitle#find should return a correct title (string)', () => {
|
||||
// [input, expected]
|
||||
const testCases = [
|
||||
['# hoge\nfuga', '# hoge'],
|
||||
@@ -20,15 +19,11 @@ test('findNoteTitle#find should return a correct title (string)', t => {
|
||||
|
||||
testCases.forEach(testCase => {
|
||||
const [input, expected] = testCase
|
||||
t.is(
|
||||
findNoteTitle(input, false),
|
||||
expected,
|
||||
`Test for find() input: ${input} expected: ${expected}`
|
||||
)
|
||||
expect(findNoteTitle(input, false)).toBe(expected)
|
||||
})
|
||||
})
|
||||
|
||||
test('findNoteTitle#find should ignore front matter when enableFrontMatterTitle=false', t => {
|
||||
test('findNoteTitle#find should ignore front matter when enableFrontMatterTitle=false', () => {
|
||||
// [input, expected]
|
||||
const testCases = [
|
||||
['---\nlayout: test\ntitle: hoge hoge hoge \n---\n# fuga', '# fuga'],
|
||||
@@ -38,15 +33,11 @@ test('findNoteTitle#find should ignore front matter when enableFrontMatterTitle
|
||||
|
||||
testCases.forEach(testCase => {
|
||||
const [input, expected] = testCase
|
||||
t.is(
|
||||
findNoteTitle(input, false),
|
||||
expected,
|
||||
`Test for find() input: ${input} expected: ${expected}`
|
||||
)
|
||||
expect(findNoteTitle(input, false)).toBe(expected)
|
||||
})
|
||||
})
|
||||
|
||||
test('findNoteTitle#find should respect front matter when enableFrontMatterTitle=true', t => {
|
||||
test('findNoteTitle#find should respect front matter when enableFrontMatterTitle=true', () => {
|
||||
// [input, expected]
|
||||
const testCases = [
|
||||
[
|
||||
@@ -59,15 +50,11 @@ test('findNoteTitle#find should respect front matter when enableFrontMatterTitl
|
||||
|
||||
testCases.forEach(testCase => {
|
||||
const [input, expected] = testCase
|
||||
t.is(
|
||||
findNoteTitle(input, true),
|
||||
expected,
|
||||
`Test for find() input: ${input} expected: ${expected}`
|
||||
)
|
||||
expect(findNoteTitle(input, true)).toBe(expected)
|
||||
})
|
||||
})
|
||||
|
||||
test('findNoteTitle#find should respect frontMatterTitleField when provided', t => {
|
||||
test('findNoteTitle#find should respect frontMatterTitleField when provided', () => {
|
||||
// [input, expected]
|
||||
const testCases = [
|
||||
['---\ntitle: hoge\n---\n# fuga', '# fuga'],
|
||||
@@ -76,10 +63,6 @@ test('findNoteTitle#find should respect frontMatterTitleField when provided', t
|
||||
|
||||
testCases.forEach(testCase => {
|
||||
const [input, expected] = testCase
|
||||
t.is(
|
||||
findNoteTitle(input, true, 'custom'),
|
||||
expected,
|
||||
`Test for find() input: ${input} expected: ${expected}`
|
||||
)
|
||||
expect(findNoteTitle(input, true, 'custom')).toBe(expected)
|
||||
})
|
||||
})
|
||||
@@ -1,8 +1,7 @@
|
||||
const test = require('ava')
|
||||
const { getTodoStatus } = require('browser/lib/getTodoStatus')
|
||||
|
||||
// Unit test
|
||||
test('getTodoStatus should return a correct hash object', t => {
|
||||
test('getTodoStatus should return a correct hash object', () => {
|
||||
// [input, expected]
|
||||
const testCases = [
|
||||
['', { total: 0, completed: 0 }],
|
||||
@@ -40,15 +39,7 @@ test('getTodoStatus should return a correct hash object', t => {
|
||||
|
||||
testCases.forEach(testCase => {
|
||||
const [input, expected] = testCase
|
||||
t.is(
|
||||
getTodoStatus(input).total,
|
||||
expected.total,
|
||||
`Test for getTodoStatus() input: ${input} expected: ${expected.total}`
|
||||
)
|
||||
t.is(
|
||||
getTodoStatus(input).completed,
|
||||
expected.completed,
|
||||
`Test for getTodoStatus() input: ${input} expected: ${expected.completed}`
|
||||
)
|
||||
expect(getTodoStatus(input).total).toBe(expected.total)
|
||||
expect(getTodoStatus(input).completed).toBe(expected.completed)
|
||||
})
|
||||
})
|
||||
@@ -1,11 +1,10 @@
|
||||
/**
|
||||
* @fileoverview Unit test for browser/lib/htmlTextHelper
|
||||
*/
|
||||
const test = require('ava')
|
||||
const htmlTextHelper = require('browser/lib/htmlTextHelper')
|
||||
|
||||
// Unit test
|
||||
test('htmlTextHelper#decodeEntities should return encoded text (string)', t => {
|
||||
test('htmlTextHelper#decodeEntities should return encoded text (string)', () => {
|
||||
// [input, expected]
|
||||
const testCases = [
|
||||
['<a href=', '<a href='],
|
||||
@@ -21,15 +20,11 @@ test('htmlTextHelper#decodeEntities should return encoded text (string)', t => {
|
||||
|
||||
testCases.forEach(testCase => {
|
||||
const [input, expected] = testCase
|
||||
t.is(
|
||||
htmlTextHelper.decodeEntities(input),
|
||||
expected,
|
||||
`Test for decodeEntities() input: ${input} expected: ${expected}`
|
||||
)
|
||||
expect(htmlTextHelper.decodeEntities(input)).toBe(expected)
|
||||
})
|
||||
})
|
||||
|
||||
test('htmlTextHelper#decodeEntities() should return decoded text (string)', t => {
|
||||
test('htmlTextHelper#decodeEntities() should return decoded text (string)', () => {
|
||||
// [input, expected]
|
||||
const testCases = [
|
||||
['<a href=', '<a href='],
|
||||
@@ -44,16 +39,12 @@ test('htmlTextHelper#decodeEntities() should return decoded text (string)', t =>
|
||||
|
||||
testCases.forEach(testCase => {
|
||||
const [input, expected] = testCase
|
||||
t.is(
|
||||
htmlTextHelper.encodeEntities(input),
|
||||
expected,
|
||||
`Test for encodeEntities() input: ${input} expected: ${expected}`
|
||||
)
|
||||
expect(htmlTextHelper.encodeEntities(input)).toBe(expected)
|
||||
})
|
||||
})
|
||||
|
||||
// Integration test
|
||||
test(t => {
|
||||
test(() => {
|
||||
const testCases = [
|
||||
"var test = 'test'",
|
||||
"<a href='https://boostnote.io'>Boostnote",
|
||||
@@ -63,10 +54,6 @@ test(t => {
|
||||
testCases.forEach(testCase => {
|
||||
const encodedText = htmlTextHelper.encodeEntities(testCase)
|
||||
const decodedText = htmlTextHelper.decodeEntities(encodedText)
|
||||
t.is(
|
||||
decodedText,
|
||||
testCase,
|
||||
'Integration test through encodedText() and decodedText()'
|
||||
)
|
||||
expect(decodedText).toBe(testCase)
|
||||
})
|
||||
})
|
||||
@@ -1,10 +1,9 @@
|
||||
/**
|
||||
* @fileoverview Unit test for browser/lib/markdown
|
||||
*/
|
||||
const test = require('ava')
|
||||
const markdown = require('browser/lib/markdownTextHelper')
|
||||
|
||||
test(t => {
|
||||
test(() => {
|
||||
// [input, expected]
|
||||
const testCases = [
|
||||
// List
|
||||
@@ -42,10 +41,6 @@ test(t => {
|
||||
|
||||
testCases.forEach(testCase => {
|
||||
const [input, expected] = testCase
|
||||
t.is(
|
||||
markdown.strip(input),
|
||||
expected,
|
||||
`Test for strip() input: ${input} expected: ${expected}`
|
||||
)
|
||||
expect(markdown.strip(input)).toBe(expected)
|
||||
})
|
||||
})
|
||||
@@ -4,11 +4,10 @@
|
||||
|
||||
import CodeMirror from 'codemirror'
|
||||
require('codemirror/addon/search/searchcursor.js')
|
||||
const test = require('ava')
|
||||
const markdownToc = require('browser/lib/markdown-toc-generator')
|
||||
const EOL = require('os').EOL
|
||||
|
||||
test(t => {
|
||||
test(() => {
|
||||
/**
|
||||
* Contains array of test cases in format :
|
||||
* [
|
||||
@@ -261,15 +260,11 @@ this is a text
|
||||
const expectedToc = testCase[2].trim()
|
||||
const generatedToc = markdownToc.generate(inputMd)
|
||||
|
||||
t.is(
|
||||
generatedToc,
|
||||
expectedToc,
|
||||
`generate test : ${title} , generated : ${EOL}${generatedToc}, expected : ${EOL}${expectedToc}`
|
||||
)
|
||||
expect(generatedToc).toBe(expectedToc)
|
||||
})
|
||||
})
|
||||
|
||||
test(t => {
|
||||
test(() => {
|
||||
/**
|
||||
* Contains array of test cases in format :
|
||||
* [
|
||||
@@ -667,10 +662,6 @@ this is a level one text
|
||||
editor.setCursor(cursor)
|
||||
markdownToc.generateInEditor(editor)
|
||||
|
||||
t.is(
|
||||
expectedMd,
|
||||
editor.getValue(),
|
||||
`generateInEditor test : ${title} , generated : ${EOL}${editor.getValue()}, expected : ${EOL}${expectedMd}`
|
||||
)
|
||||
expect(expectedMd).toBe(editor.getValue())
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,17 @@
|
||||
import test from 'ava'
|
||||
jest.mock(
|
||||
'electron',
|
||||
() => {
|
||||
return {
|
||||
remote: {
|
||||
app: {
|
||||
getPath: jest.fn().mockReturnValue('.')
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{ virtual: true }
|
||||
)
|
||||
|
||||
import Markdown from 'browser/lib/markdown'
|
||||
import markdownFixtures from '../fixtures/markdowns'
|
||||
|
||||
@@ -6,100 +19,100 @@ import markdownFixtures from '../fixtures/markdowns'
|
||||
// To test markdown options, initialize a new instance in your test case
|
||||
const md = new Markdown()
|
||||
|
||||
test('Markdown.render() should renders markdown correctly', t => {
|
||||
test('Markdown.render() should renders markdown correctly', () => {
|
||||
const rendered = md.render(markdownFixtures.basic)
|
||||
t.snapshot(rendered)
|
||||
expect(rendered).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('Markdown.render() should renders codeblock correctly', t => {
|
||||
test('Markdown.render() should renders codeblock correctly', () => {
|
||||
const rendered = md.render(markdownFixtures.codeblock)
|
||||
t.snapshot(rendered)
|
||||
expect(rendered).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('Markdown.render() should renders KaTeX correctly', t => {
|
||||
test('Markdown.render() should renders KaTeX correctly', () => {
|
||||
const rendered = md.render(markdownFixtures.katex)
|
||||
t.snapshot(rendered)
|
||||
expect(rendered).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('Markdown.render() should renders checkboxes', t => {
|
||||
test('Markdown.render() should renders checkboxes', () => {
|
||||
const rendered = md.render(markdownFixtures.checkboxes)
|
||||
t.snapshot(rendered)
|
||||
expect(rendered).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('Markdown.render() should text with quotes correctly', t => {
|
||||
test('Markdown.render() should text with quotes correctly', () => {
|
||||
const renderedSmartQuotes = md.render(markdownFixtures.smartQuotes)
|
||||
t.snapshot(renderedSmartQuotes)
|
||||
expect(renderedSmartQuotes).toMatchSnapshot()
|
||||
|
||||
const newmd = new Markdown({ typographer: false })
|
||||
const renderedNonSmartQuotes = newmd.render(markdownFixtures.smartQuotes)
|
||||
t.snapshot(renderedNonSmartQuotes)
|
||||
expect(renderedNonSmartQuotes).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('Markdown.render() should render line breaks correctly', t => {
|
||||
test('Markdown.render() should render line breaks correctly', () => {
|
||||
const renderedBreaks = md.render(markdownFixtures.breaks)
|
||||
t.snapshot(renderedBreaks)
|
||||
expect(renderedBreaks).toMatchSnapshot()
|
||||
|
||||
const newmd = new Markdown({ breaks: false })
|
||||
const renderedNonBreaks = newmd.render(markdownFixtures.breaks)
|
||||
t.snapshot(renderedNonBreaks)
|
||||
expect(renderedNonBreaks).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('Markdown.render() should renders abbrevations correctly', t => {
|
||||
test('Markdown.render() should renders abbrevations correctly', () => {
|
||||
const rendered = md.render(markdownFixtures.abbrevations)
|
||||
t.snapshot(rendered)
|
||||
expect(rendered).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('Markdown.render() should renders sub correctly', t => {
|
||||
test('Markdown.render() should renders sub correctly', () => {
|
||||
const rendered = md.render(markdownFixtures.subTexts)
|
||||
t.snapshot(rendered)
|
||||
expect(rendered).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('Markdown.render() should renders sup correctly', t => {
|
||||
test('Markdown.render() should renders sup correctly', () => {
|
||||
const rendered = md.render(markdownFixtures.supTexts)
|
||||
t.snapshot(rendered)
|
||||
expect(rendered).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('Markdown.render() should renders definition lists correctly', t => {
|
||||
test('Markdown.render() should renders definition lists correctly', () => {
|
||||
const rendered = md.render(markdownFixtures.deflists)
|
||||
t.snapshot(rendered)
|
||||
expect(rendered).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('Markdown.render() should render shortcuts correctly', t => {
|
||||
test('Markdown.render() should render shortcuts correctly', () => {
|
||||
const rendered = md.render(markdownFixtures.shortcuts)
|
||||
t.snapshot(rendered)
|
||||
expect(rendered).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('Markdown.render() should render footnote correctly', t => {
|
||||
test('Markdown.render() should render footnote correctly', () => {
|
||||
const rendered = md.render(markdownFixtures.footnote)
|
||||
t.snapshot(rendered)
|
||||
expect(rendered).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('Markdown.render() should renders [TOC] placholder correctly', t => {
|
||||
test('Markdown.render() should renders [TOC] placholder correctly', () => {
|
||||
const rendered = md.render(markdownFixtures.tocPlaceholder)
|
||||
t.snapshot(rendered)
|
||||
expect(rendered).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('Markdown.render() should render PlantUML MindMaps correctly', t => {
|
||||
test('Markdown.render() should render PlantUML MindMaps correctly', () => {
|
||||
const rendered = md.render(markdownFixtures.plantUmlMindMap)
|
||||
t.snapshot(rendered)
|
||||
expect(rendered).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('Markdown.render() should render PlantUML Gantt correctly', t => {
|
||||
test('Markdown.render() should render PlantUML Gantt correctly', () => {
|
||||
const rendered = md.render(markdownFixtures.plantUmlGantt)
|
||||
t.snapshot(rendered)
|
||||
expect(rendered).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('Markdown.render() should render PlantUML WBS correctly', t => {
|
||||
test('Markdown.render() should render PlantUML WBS correctly', () => {
|
||||
const rendered = md.render(markdownFixtures.plantUmlWbs)
|
||||
t.snapshot(rendered)
|
||||
expect(rendered).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('Markdown.render() should render PlantUML Umls correctly', t => {
|
||||
test('Markdown.render() should render PlantUML Umls correctly', () => {
|
||||
const rendered = md.render(markdownFixtures.plantUmlUml)
|
||||
t.snapshot(rendered)
|
||||
expect(rendered).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('Markdown.render() should render PlantUML Ditaa correctly', t => {
|
||||
test('Markdown.render() should render PlantUML Ditaa correctly', () => {
|
||||
const rendered = md.render(markdownFixtures.plantUmlDitaa)
|
||||
t.snapshot(rendered)
|
||||
expect(rendered).toMatchSnapshot()
|
||||
})
|
||||
@@ -1,19 +1,17 @@
|
||||
/**
|
||||
* @fileoverview Unit test for browser/lib/normalizeEditorFontFamily
|
||||
*/
|
||||
import test from 'ava'
|
||||
import normalizeEditorFontFamily from '../../browser/lib/normalizeEditorFontFamily'
|
||||
import consts from '../../browser/lib/consts'
|
||||
const defaultEditorFontFamily = consts.DEFAULT_EDITOR_FONT_FAMILY
|
||||
|
||||
test('normalizeEditorFontFamily() should return default font family (string[])', t => {
|
||||
t.is(normalizeEditorFontFamily(), defaultEditorFontFamily.join(', '))
|
||||
test('normalizeEditorFontFamily() should return default font family (string[])', () => {
|
||||
expect(normalizeEditorFontFamily()).toBe(defaultEditorFontFamily.join(', '))
|
||||
})
|
||||
|
||||
test('normalizeEditorFontFamily(["hoge", "huga"]) should return default font family connected with arg.', t => {
|
||||
test('normalizeEditorFontFamily(["hoge", "huga"]) should return default font family connected with arg.', () => {
|
||||
const arg = 'font1, font2'
|
||||
t.is(
|
||||
normalizeEditorFontFamily(arg),
|
||||
expect(normalizeEditorFontFamily(arg)).toBe(
|
||||
`${arg}, ${defaultEditorFontFamily.join(', ')}`
|
||||
)
|
||||
})
|
||||
@@ -1,9 +1,8 @@
|
||||
const test = require('ava')
|
||||
const path = require('path')
|
||||
const { parse } = require('browser/lib/RcParser')
|
||||
|
||||
// Unit test
|
||||
test('RcParser should return a json object', t => {
|
||||
test('RcParser should return a json object', () => {
|
||||
const validJson = {
|
||||
editor: { keyMap: 'vim', switchPreview: 'BLUR', theme: 'monokai' },
|
||||
hotkey: { toggleMain: 'Control + L' },
|
||||
@@ -51,20 +50,12 @@ test('RcParser should return a json object', t => {
|
||||
|
||||
validTestCases.forEach(validTestCase => {
|
||||
const [input, expected] = validTestCase
|
||||
t.is(
|
||||
parse(filePath(input)).editor.keyMap,
|
||||
expected.editor.keyMap,
|
||||
`Test for getTodoStatus() input: ${input} expected: ${expected.keyMap}`
|
||||
)
|
||||
expect(parse(filePath(input)).editor.keyMap).toBe(expected.editor.keyMap)
|
||||
})
|
||||
|
||||
invalidTestCases.forEach(invalidTestCase => {
|
||||
const [input, expected] = invalidTestCase
|
||||
t.is(
|
||||
parse(filePath(input)).editor,
|
||||
expected.editor,
|
||||
`Test for getTodoStatus() input: ${input} expected: ${expected.editor}`
|
||||
)
|
||||
expect(parse(filePath(input)).editor).toBe(expected.editor)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import test from 'ava'
|
||||
import searchFromNotes from 'browser/lib/search'
|
||||
import { dummyNote } from '../fixtures/TestDummy'
|
||||
import _ from 'lodash'
|
||||
@@ -11,7 +10,7 @@ const pickContents = notes =>
|
||||
let notes = []
|
||||
let note1, note2, note3
|
||||
|
||||
test.before(t => {
|
||||
beforeAll(() => {
|
||||
const data1 = { type: 'MARKDOWN_NOTE', content: 'content1', tags: ['tag1'] }
|
||||
const data2 = {
|
||||
type: 'MARKDOWN_NOTE',
|
||||
@@ -27,7 +26,7 @@ test.before(t => {
|
||||
notes = [note1, note2, note3]
|
||||
})
|
||||
|
||||
test('it can find notes by tags and words', t => {
|
||||
test('it can find notes by tags and words', () => {
|
||||
// [input, expected content (Array)]
|
||||
const testWithTags = [
|
||||
['#tag1', [note1.content, note2.content, note3.content]],
|
||||
@@ -49,6 +48,8 @@ test('it can find notes by tags and words', t => {
|
||||
testCases.forEach(testCase => {
|
||||
const [input, expectedContents] = testCase
|
||||
const results = searchFromNotes(notes, input)
|
||||
t.true(_.isEqual(pickContents(results).sort(), expectedContents.sort()))
|
||||
expect(
|
||||
_.isEqual(pickContents(results).sort(), expectedContents.sort())
|
||||
).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -1,58 +1,57 @@
|
||||
import test from 'ava'
|
||||
import slugify from 'browser/lib/slugify'
|
||||
|
||||
test('alphabet and digit', t => {
|
||||
test('alphabet and digit', () => {
|
||||
const upperAlphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
||||
const lowerAlphabet = 'abcdefghijklmnopqrstuvwxyz'
|
||||
const digit = '0123456789'
|
||||
const testCase = upperAlphabet + lowerAlphabet + digit
|
||||
const decodeSlug = decodeURI(slugify(testCase))
|
||||
|
||||
t.true(decodeSlug === testCase)
|
||||
expect(decodeSlug === testCase).toBe(true)
|
||||
})
|
||||
|
||||
test('should delete unavailable symbols', t => {
|
||||
test('should delete unavailable symbols', () => {
|
||||
const availableSymbols = '_-'
|
||||
const testCase = availableSymbols + "][!'#$%&()*+,./:;<=>?@\\^{|}~`"
|
||||
const decodeSlug = decodeURI(slugify(testCase))
|
||||
|
||||
t.true(decodeSlug === availableSymbols)
|
||||
expect(decodeSlug === availableSymbols).toBe(true)
|
||||
})
|
||||
|
||||
test('should convert from white spaces between words to hyphens', t => {
|
||||
test('should convert from white spaces between words to hyphens', () => {
|
||||
const testCase = 'This is one'
|
||||
const expectedString = 'This-is-one'
|
||||
const decodeSlug = decodeURI(slugify(testCase))
|
||||
|
||||
t.true(decodeSlug === expectedString)
|
||||
expect(decodeSlug === expectedString).toBe(true)
|
||||
})
|
||||
|
||||
test('should remove leading white spaces', t => {
|
||||
test('should remove leading white spaces', () => {
|
||||
const testCase = ' This is one'
|
||||
const expectedString = 'This-is-one'
|
||||
const decodeSlug = decodeURI(slugify(testCase))
|
||||
|
||||
t.true(decodeSlug === expectedString)
|
||||
expect(decodeSlug === expectedString).toBe(true)
|
||||
})
|
||||
|
||||
test('should remove trailing white spaces', t => {
|
||||
test('should remove trailing white spaces', () => {
|
||||
const testCase = 'This is one '
|
||||
const expectedString = 'This-is-one'
|
||||
const decodeSlug = decodeURI(slugify(testCase))
|
||||
|
||||
t.true(decodeSlug === expectedString)
|
||||
expect(decodeSlug === expectedString).toBe(true)
|
||||
})
|
||||
|
||||
test('2-byte charactor support', t => {
|
||||
test('2-byte charactor support', () => {
|
||||
const testCase = '菠萝芒果テストÀžƁƵ'
|
||||
const decodeSlug = decodeURI(slugify(testCase))
|
||||
|
||||
t.true(decodeSlug === testCase)
|
||||
expect(decodeSlug === testCase).toBe(true)
|
||||
})
|
||||
|
||||
test('emoji', t => {
|
||||
test('emoji', () => {
|
||||
const testCase = '🌸'
|
||||
const decodeSlug = decodeURI(slugify(testCase))
|
||||
|
||||
t.true(decodeSlug === testCase)
|
||||
expect(decodeSlug === testCase).toBe(true)
|
||||
})
|
||||
Binary file not shown.
103
tests/lib/themeManager.test.js
Normal file
103
tests/lib/themeManager.test.js
Normal file
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* @fileoverview Unit test for browser/main/lib/ThemeManager.js
|
||||
*/
|
||||
const { chooseTheme, applyTheme } = require('browser/main/lib/ThemeManager')
|
||||
jest.mock('../../browser/main/lib/ConfigManager', () => {
|
||||
return {
|
||||
set: () => {}
|
||||
}
|
||||
})
|
||||
|
||||
const originalDate = Date
|
||||
let context = {}
|
||||
|
||||
beforeAll(() => {
|
||||
const constantDate = new Date('2017-11-27T14:33:42')
|
||||
global.Date = class extends Date {
|
||||
constructor() {
|
||||
super()
|
||||
return constantDate
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
context = {
|
||||
ui: {
|
||||
theme: 'white',
|
||||
scheduledTheme: 'dark',
|
||||
enableScheduleTheme: true,
|
||||
defaultTheme: 'monokai'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
global.Date = originalDate
|
||||
})
|
||||
|
||||
test("enableScheduleTheme is false, theme shouldn't change", () => {
|
||||
context.ui.enableScheduleTheme = false
|
||||
|
||||
const beforeTheme = context.ui.theme
|
||||
chooseTheme(context)
|
||||
const afterTheme = context.ui.theme
|
||||
|
||||
expect(afterTheme).toBe(beforeTheme)
|
||||
})
|
||||
|
||||
// NOT IN SCHEDULE
|
||||
test("scheduleEnd is bigger than scheduleStart and not in schedule, theme shouldn't change", () => {
|
||||
const beforeTheme = context.ui.defaultTheme
|
||||
context.ui.scheduleStart = 720 // 12:00
|
||||
context.ui.scheduleEnd = 870 // 14:30
|
||||
chooseTheme(context)
|
||||
const afterTheme = context.ui.theme
|
||||
|
||||
expect(afterTheme).toBe(beforeTheme)
|
||||
})
|
||||
|
||||
test("scheduleStart is bigger than scheduleEnd and not in schedule, theme shouldn't change", () => {
|
||||
const beforeTheme = context.ui.defaultTheme
|
||||
context.ui.scheduleStart = 960 // 16:00
|
||||
context.ui.scheduleEnd = 600 // 10:00
|
||||
chooseTheme(context)
|
||||
const afterTheme = context.ui.theme
|
||||
|
||||
expect(afterTheme).toBe(beforeTheme)
|
||||
})
|
||||
|
||||
// IN SCHEDULE
|
||||
test('scheduleEnd is bigger than scheduleStart and in schedule, theme should change', () => {
|
||||
const beforeTheme = context.ui.scheduledTheme
|
||||
context.ui.scheduleStart = 720 // 12:00
|
||||
context.ui.scheduleEnd = 900 // 15:00
|
||||
chooseTheme(context)
|
||||
const afterTheme = context.ui.theme
|
||||
|
||||
expect(afterTheme).toBe(beforeTheme)
|
||||
})
|
||||
|
||||
test('scheduleStart is bigger than scheduleEnd and in schedule, theme should change', () => {
|
||||
const beforeTheme = context.ui.scheduledTheme
|
||||
context.ui.scheduleStart = 1200 // 20:00
|
||||
context.ui.scheduleEnd = 900 // 15:00
|
||||
chooseTheme(context)
|
||||
const afterTheme = context.ui.theme
|
||||
|
||||
expect(afterTheme).toBe(beforeTheme)
|
||||
})
|
||||
|
||||
test("theme to apply is not a supported theme, theme shouldn't change", () => {
|
||||
applyTheme('notATheme')
|
||||
const afterTheme = document.body.dataset.theme
|
||||
|
||||
expect(afterTheme).toBe('default')
|
||||
})
|
||||
|
||||
test('theme to apply is a supported theme, theme should change', () => {
|
||||
applyTheme(context.ui.defaultTheme)
|
||||
const afterTheme = document.body.dataset.theme
|
||||
|
||||
expect(afterTheme).toBe(context.ui.defaultTheme)
|
||||
})
|
||||
122
yarn.lock
122
yarn.lock
@@ -1966,6 +1966,11 @@ combined-stream@1.0.6, combined-stream@~1.0.5:
|
||||
dependencies:
|
||||
delayed-stream "~1.0.0"
|
||||
|
||||
command-exists@^1.2.9:
|
||||
version "1.2.9"
|
||||
resolved "https://registry.yarnpkg.com/command-exists/-/command-exists-1.2.9.tgz#c50725af3808c8ab0260fd60b01fbfa25b954f69"
|
||||
integrity sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==
|
||||
|
||||
commander@2:
|
||||
version "2.16.0"
|
||||
resolved "http://registry.npm.taobao.org/commander/download/commander-2.16.0.tgz#f16390593996ceb4f3eeb020b31d78528f7f8a50"
|
||||
@@ -2583,7 +2588,44 @@ d3-zoom@1:
|
||||
d3-selection "1"
|
||||
d3-transition "1"
|
||||
|
||||
d3@^5.12, d3@^5.7.0:
|
||||
d3@^5.14:
|
||||
version "5.16.0"
|
||||
resolved "https://registry.yarnpkg.com/d3/-/d3-5.16.0.tgz#9c5e8d3b56403c79d4ed42fbd62f6113f199c877"
|
||||
integrity sha512-4PL5hHaHwX4m7Zr1UapXW23apo6pexCgdetdJ5kTmADpG/7T9Gkxw0M0tf/pjoB63ezCCm0u5UaFYy2aMt0Mcw==
|
||||
dependencies:
|
||||
d3-array "1"
|
||||
d3-axis "1"
|
||||
d3-brush "1"
|
||||
d3-chord "1"
|
||||
d3-collection "1"
|
||||
d3-color "1"
|
||||
d3-contour "1"
|
||||
d3-dispatch "1"
|
||||
d3-drag "1"
|
||||
d3-dsv "1"
|
||||
d3-ease "1"
|
||||
d3-fetch "1"
|
||||
d3-force "1"
|
||||
d3-format "1"
|
||||
d3-geo "1"
|
||||
d3-hierarchy "1"
|
||||
d3-interpolate "1"
|
||||
d3-path "1"
|
||||
d3-polygon "1"
|
||||
d3-quadtree "1"
|
||||
d3-random "1"
|
||||
d3-scale "2"
|
||||
d3-scale-chromatic "1"
|
||||
d3-selection "1"
|
||||
d3-shape "1"
|
||||
d3-time "1"
|
||||
d3-time-format "2"
|
||||
d3-timer "1"
|
||||
d3-transition "1"
|
||||
d3-voronoi "1"
|
||||
d3-zoom "1"
|
||||
|
||||
d3@^5.7.0:
|
||||
version "5.12.0"
|
||||
resolved "https://registry.yarnpkg.com/d3/-/d3-5.12.0.tgz#0ddeac879c28c882317cd439b495290acd59ab61"
|
||||
integrity sha512-flYVMoVuhPFHd9zVCe2BxIszUWqBcd5fvQGMNRmSiBrgdnh6Vlruh60RJQTouAK9xPbOB0plxMvBm4MoyODXNg==
|
||||
@@ -2626,13 +2668,14 @@ d@1:
|
||||
dependencies:
|
||||
es5-ext "^0.10.9"
|
||||
|
||||
dagre-d3@dagrejs/dagre-d3:
|
||||
version "0.6.4-pre"
|
||||
resolved "https://codeload.github.com/dagrejs/dagre-d3/tar.gz/e1a00e5cb518f5d2304a35647e024f31d178e55b"
|
||||
dagre-d3@^0.6.4:
|
||||
version "0.6.4"
|
||||
resolved "https://registry.yarnpkg.com/dagre-d3/-/dagre-d3-0.6.4.tgz#0728d5ce7f177ca2337df141ceb60fbe6eeb7b29"
|
||||
integrity sha512-e/6jXeCP7/ptlAM48clmX4xTZc5Ek6T6kagS7Oz2HrYSdqcLZFLqpAfh7ldbZRFfxCZVyh61NEPR08UQRVxJzQ==
|
||||
dependencies:
|
||||
d3 "^5.12"
|
||||
dagre "^0.8.4"
|
||||
graphlib "^2.1.7"
|
||||
d3 "^5.14"
|
||||
dagre "^0.8.5"
|
||||
graphlib "^2.1.8"
|
||||
lodash "^4.17.15"
|
||||
|
||||
dagre@^0.8.4:
|
||||
@@ -2643,6 +2686,14 @@ dagre@^0.8.4:
|
||||
graphlib "^2.1.7"
|
||||
lodash "^4.17.4"
|
||||
|
||||
dagre@^0.8.5:
|
||||
version "0.8.5"
|
||||
resolved "https://registry.yarnpkg.com/dagre/-/dagre-0.8.5.tgz#ba30b0055dac12b6c1fcc247817442777d06afee"
|
||||
integrity sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==
|
||||
dependencies:
|
||||
graphlib "^2.1.8"
|
||||
lodash "^4.17.15"
|
||||
|
||||
dashdash@^1.12.0:
|
||||
version "1.14.1"
|
||||
resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
|
||||
@@ -3130,6 +3181,13 @@ entities@^1.1.1, entities@~1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0"
|
||||
|
||||
entity-decode@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/entity-decode/-/entity-decode-2.0.2.tgz#e4f807e52c3294246e9347d1f2b02b07fd5f92e7"
|
||||
integrity sha512-5CCY/3ci4MC1m2jlumNjWd7VBFt4VfFnmSqSNmVcXq4gxM3Vmarxtt+SvmBnzwLS669MWdVuXboNVj1qN2esVg==
|
||||
dependencies:
|
||||
he "^1.1.1"
|
||||
|
||||
env-paths@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-1.0.0.tgz#4168133b42bb05c38a35b1ae4397c8298ab369e0"
|
||||
@@ -4302,6 +4360,13 @@ graphlib@^2.1.7:
|
||||
dependencies:
|
||||
lodash "^4.17.5"
|
||||
|
||||
graphlib@^2.1.8:
|
||||
version "2.1.8"
|
||||
resolved "https://registry.yarnpkg.com/graphlib/-/graphlib-2.1.8.tgz#5761d414737870084c92ec7b5dbcb0592c9d35da"
|
||||
integrity sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==
|
||||
dependencies:
|
||||
lodash "^4.17.15"
|
||||
|
||||
gray-matter@^2.1.0:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/gray-matter/-/gray-matter-2.1.1.tgz#3042d9adec2a1ded6a7707a9ed2380f8a17a430e"
|
||||
@@ -4490,7 +4555,7 @@ has@^1.0.1:
|
||||
dependencies:
|
||||
function-bind "^1.0.2"
|
||||
|
||||
he@^1.2.0:
|
||||
he@^1.1.1, he@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
|
||||
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
|
||||
@@ -5987,8 +6052,9 @@ locate-path@^3.0.0:
|
||||
path-exists "^3.0.0"
|
||||
|
||||
lodash-es@^4.2.1:
|
||||
version "4.17.10"
|
||||
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.10.tgz#62cd7104cdf5dd87f235a837f0ede0e8e5117e05"
|
||||
version "4.17.15"
|
||||
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.15.tgz#21bd96839354412f23d7a10340e5eac6ee455d78"
|
||||
integrity sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ==
|
||||
|
||||
lodash-move@^1.1.1:
|
||||
version "1.1.1"
|
||||
@@ -6076,8 +6142,9 @@ lodash.merge@^4.6.0:
|
||||
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.1.tgz#adc25d9cb99b9391c59624f379fbba60d7111d54"
|
||||
|
||||
lodash.mergewith@^4.6.0:
|
||||
version "4.6.1"
|
||||
resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz#639057e726c3afbdb3e7d42741caa8d6e4335927"
|
||||
version "4.6.2"
|
||||
resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz#617121f89ac55f59047c7aec1ccd6654c6590f55"
|
||||
integrity sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==
|
||||
|
||||
lodash.some@^4.5.1:
|
||||
version "4.6.0"
|
||||
@@ -6106,15 +6173,10 @@ lodash.uniq@^4.5.0:
|
||||
version "4.5.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
|
||||
|
||||
lodash@^4.0.0, lodash@^4.0.1, lodash@^4.12.0, lodash@^4.13.1, lodash@^4.16.6, lodash@^4.17.10, lodash@^4.17.13, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.5.1, lodash@^4.6.1:
|
||||
version "4.17.13"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.13.tgz#0bdc3a6adc873d2f4e0c4bac285df91b64fc7b93"
|
||||
integrity sha512-vm3/XWXfWtRua0FkUyEHBZy8kCPjErNBT9fJx8Zvs+U6zjqPbTUOpkaoum3O5uiA8sm+yNMHXfYkTUHFoMxFNA==
|
||||
|
||||
lodash@^4.13.0, lodash@^4.17.11, lodash@^4.17.15:
|
||||
version "4.17.15"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
|
||||
integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
|
||||
lodash@^4.0.0, lodash@^4.0.1, lodash@^4.12.0, lodash@^4.13.0, lodash@^4.13.1, lodash@^4.16.6, lodash@^4.17.10, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.5.1, lodash@^4.6.1:
|
||||
version "4.17.19"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b"
|
||||
integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==
|
||||
|
||||
lodash@~0.9.2:
|
||||
version "0.9.2"
|
||||
@@ -6398,22 +6460,21 @@ merge@^1.1.3:
|
||||
resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.1.tgz#38bebf80c3220a8a487b6fcfb3941bb11720c145"
|
||||
integrity sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ==
|
||||
|
||||
mermaid@^8.4.2:
|
||||
version "8.4.2"
|
||||
resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-8.4.2.tgz#91d3d8e9541e72eed7a78d0e882db11564fab3bb"
|
||||
integrity sha512-vYSCP2u4XkOnjliWz/QIYwvzF/znQAq22vWJJ3YV40SnwV2JQyHblnwwNYXCprkXw7XfwBKDpSNaJ3HP4WfnZw==
|
||||
mermaid@^8.5.2:
|
||||
version "8.5.2"
|
||||
resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-8.5.2.tgz#0f1914cda53d4ea5377380e5ce07a38bef2ea7e8"
|
||||
integrity sha512-I+s+8/RzlazF3dGOhDUfU/ERkUV4zfIlTWb3703jNx+2lfACs+4AdY9ULQaw6BPWzW3gB+XlXFOOX/m/vqujIA==
|
||||
dependencies:
|
||||
"@braintree/sanitize-url" "^3.1.0"
|
||||
crypto-random-string "^3.0.1"
|
||||
d3 "^5.7.0"
|
||||
dagre "^0.8.4"
|
||||
dagre-d3 dagrejs/dagre-d3
|
||||
dagre-d3 "^0.6.4"
|
||||
entity-decode "^2.0.2"
|
||||
graphlib "^2.1.7"
|
||||
he "^1.2.0"
|
||||
lodash "^4.17.11"
|
||||
minify "^4.1.1"
|
||||
moment-mini "^2.22.1"
|
||||
prettier "^1.18.2"
|
||||
scope-css "^1.2.1"
|
||||
|
||||
methods@~1.1.2:
|
||||
@@ -10025,8 +10086,9 @@ websocket-driver@>=0.5.1:
|
||||
websocket-extensions ">=0.1.1"
|
||||
|
||||
websocket-extensions@>=0.1.1:
|
||||
version "0.1.3"
|
||||
resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.3.tgz#5d2ff22977003ec687a4b87073dfbbac146ccf29"
|
||||
version "0.1.4"
|
||||
resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42"
|
||||
integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==
|
||||
|
||||
well-known-symbols@^1.0.0:
|
||||
version "1.0.0"
|
||||
|
||||
Reference in New Issue
Block a user