1
0
mirror of https://github.com/BoostIo/Boostnote synced 2025-12-14 18:26:26 +00:00

Compare commits

..

88 Commits

Author SHA1 Message Date
dependabot[bot]
d6fed24288 Bump hosted-git-info from 2.6.0 to 2.8.9
Bumps [hosted-git-info](https://github.com/npm/hosted-git-info) from 2.6.0 to 2.8.9.
- [Release notes](https://github.com/npm/hosted-git-info/releases)
- [Changelog](https://github.com/npm/hosted-git-info/blob/v2.8.9/CHANGELOG.md)
- [Commits](https://github.com/npm/hosted-git-info/compare/v2.6.0...v2.8.9)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-10 06:00:17 +00:00
Baptiste Augrain
58c4a78be1 avoids conflicting styles between inline codes and code blocks 2020-12-27 11:16:55 +09:00
Gonçalo Santos
2603dfc1ed Fix Analytics save bug 2020-12-12 00:15:37 +09:00
Gonçalo Santos
2df590600b AutoUpdate is auto saved 2020-12-12 00:15:37 +09:00
Gonçalo Santos
ef20a8f3e5 Remove debug statements 2020-12-12 00:15:37 +09:00
Gonçalo Santos
3e405e1abf Fix Cancel update 2020-12-12 00:15:37 +09:00
Gonçalo Santos
553832bdfa Create confirm download dialog 2020-12-12 00:15:37 +09:00
Gonçalo Santos
18d65d999a Menu item calls update-check 2020-12-12 00:15:37 +09:00
Gonçalo Santos
3b5eff582a Update not found message 2020-12-12 00:15:37 +09:00
Gonçalo Santos
85d09b3b3d Add update menu item 2020-12-12 00:15:37 +09:00
Baptiste Augrain
8958e67fcf fix unwanted deletion of attachments 2020-09-15 12:33:12 +09:00
Junyoung Choi
47b796909a v0.16.1 2020-09-04 23:47:01 +09:00
Junyoung Choi
67d76abdfa Merge branch 'master' of github.com:BoostIO/Boostnote 2020-09-04 23:45:41 +09:00
Junyoung Choi
d75d68ba72 Add email subscription form 2020-09-04 23:44:16 +09:00
Junyoung Choi
323be6b72d Merge pull request #2612 from daiyam/export-yfm
improve export
2020-07-22 18:24:27 +09:00
Junyoung Choi
031a113338 Merge branch 'master' of github.com:BoostIO/Boostnote 2020-07-20 21:07:26 +09:00
Junyoung Choi
b50c5386a6 Add BoostHub link to menu 2020-07-20 21:06:55 +09:00
Baptiste Augrain
65777b1d56 Merge branch 'master' into export-yfm 2020-07-20 14:05:21 +02:00
Junyoung Choi
fe728874ac Fix code sign script 2020-07-20 21:02:29 +09:00
Junyoung Choi
c5b4c327fa v0.16.0 2020-07-20 20:42:18 +09:00
Junyoung Choi
4c39922ead Merge pull request #2405 from daiyam/fix-scroll
Better scroll sync between the editor and the preview in the SplitEditor
2020-07-20 19:48:29 +09:00
Junyoung Choi
1cdc74a2f0 Merge pull request #2594 from daiyam/fix-autocomplete-codeblock
improve autocomplete within code blocks
2020-07-20 19:47:48 +09:00
Junyoung Choi
6213a820e6 Merge pull request #3600 from ZeroX-DG/date-iso8601
Add Date shortcut ISO 8601 format as an option in preference
2020-07-20 19:46:53 +09:00
Junyoung Choi
ce81b26d1d Merge branch 'master' into date-iso8601 2020-07-20 19:42:08 +09:00
dependabot[bot]
fae91255f9 Bump lodash from 4.17.13 to 4.17.19
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.13 to 4.17.19.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.13...4.17.19)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-20 19:28:17 +09:00
dependabot[bot]
a82a3efb14 Bump websocket-extensions from 0.1.3 to 0.1.4
Bumps [websocket-extensions](https://github.com/faye/websocket-extensions-node) from 0.1.3 to 0.1.4.
- [Release notes](https://github.com/faye/websocket-extensions-node/releases)
- [Changelog](https://github.com/faye/websocket-extensions-node/blob/master/CHANGELOG.md)
- [Commits](https://github.com/faye/websocket-extensions-node/compare/0.1.3...0.1.4)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-20 19:27:17 +09:00
Junyoung Choi
9556417447 Merge pull request #2923 from LuisReinoso/master
Add Wakatime integration
2020-07-20 19:26:53 +09:00
Baptiste Augrain
60fbb7db5d fix unclickable links 2020-07-20 19:25:53 +09:00
Baptiste Augrain
e504f8e63e fix broken nord and vulcan themes 2020-07-20 19:25:27 +09:00
Baptiste Augrain
071ce12a7e add updated yarn.lock 2020-07-20 19:23:25 +09:00
Baptiste Augrain
0decaf187c update mermaid due to missing arrowheads 2020-07-20 19:23:25 +09:00
Junyoung Choi
961644747e Update readme.md 2020-07-09 13:28:02 +09:00
ZeroX-DG
d1a81984fb fixed eslint 2020-07-02 11:46:13 +12:00
Baptiste Augrain
bd9b1306b1 fix missing isStacking flag 2020-06-26 17:55:16 +02:00
Baptiste Augrain
0ca18d8ca5 re-add missing isStacking flag 2020-06-26 03:07:25 +02:00
Baptiste Augrain
540d72696c Merge branch 'master' into fix-autocomplete-codeblock 2020-06-26 02:40:37 +02:00
Baptiste Augrain
87a530612f - use newest test
- remove useless binding
- regroup function
2020-06-25 22:50:08 +02:00
Baptiste Augrain
f4259bb4d0 fix exporting storage's notes into their own folders 2020-06-12 17:36:46 +02:00
Baptiste Augrain
aa8b589569 don't show export menu to snippet notes 2020-06-12 16:56:07 +02:00
Baptiste Augrain
febc98c101 fix exporting storage's notes as PDFs 2020-06-12 16:51:35 +02:00
Baptiste Augrain
b678c3bd89 export html is escaping html characters as the preview 2020-06-12 16:40:16 +02:00
Baptiste Augrain
80b8948433 - export untitled notes as 'Untitled' based on the language
- export notes with duplicate title as '<title> (<index>)'
2020-06-12 16:18:27 +02:00
Baptiste Augrain
5414fe3384 export tag 2020-06-12 15:53:23 +02:00
Baptiste Augrain
db4016385d Merge branch 'master' into export-yfm 2020-06-12 15:17:02 +02:00
Luis Reinoso
8ede1a4989 refactor: Change UI according requested changes.
NOTE:
Just a simple section with title Wakatime and bellow is a check box saying enable wakatime? and below it is the text box for wakatime key.
2020-06-07 11:17:02 -05:00
Luis Reinoso
76da76ae76 fix: Wakatime command name. 2020-06-07 11:01:44 -05:00
Luis Reinoso
c02ab033f4 fix: Remove extra calling function. Now call directly to check wakatime plugin. 2020-05-23 12:25:37 -05:00
Luis Reinoso
1aaba74e24 refactor: Improve sendWakatimeHeartBeat. 2020-05-23 12:21:43 -05:00
Luis Reinoso
6fe6794796 feat: Add checkbox validation to active or deactive plugin. 2020-05-16 11:13:19 -05:00
Luis Reinoso
fd3e243855 feat: Check for missing wakatime cli. And display modal and alert message. 2020-05-16 10:08:51 -05:00
Luis Reinoso
938b075bf6 fix: Lint errors. 2020-05-16 08:37:35 -05:00
Baptiste Augrain
81ac3d1748 fix scrolling with only one big paragraph 2020-05-16 14:19:22 +02:00
Baptiste Augrain
40d10eae04 Merge branch 'master' into fix-scroll 2020-05-15 02:59:00 +02:00
Baptiste Augrain
9b6a61a91c fix cursor sync 2020-05-15 02:57:00 +02:00
Baptiste Augrain
7116c305ca Merge branch 'master' into fix-scroll 2020-05-15 02:02:38 +02:00
Luis Reinoso
2ac38e9644 fix: Update prettier config with master. 2020-05-08 17:11:16 -05:00
Luis Reinoso
98d4fa0603 fix: Lint issues. 2020-05-08 10:01:30 -05:00
Luis Reinoso
2ea0514bbe Merge remote-tracking branch 'upstream/master' 2020-05-08 06:55:48 -05:00
lockee14
2b3538d3b1 add indentation and closing brace
missing indentation and brace between line 229 - 234:
before:
'Shift-Cmd-/': function (cm) {
        if (global.process.platform !== 'darwin') { return }
      [translateHotkey(hotkey.insertDateTime)]: function (cm) {
        const dateNow = new Date()
        cm.replaceSelection(dateNow.toLocaleString())
},

after:
'Shift-Cmd-/': function (cm) {
        if (global.process.platform !== 'darwin') { return }
        [translateHotkey(hotkey.insertDateTime)]: function (cm) {
          const dateNow = new Date()
          cm.replaceSelection(dateNow.toLocaleString())
        }
},
2019-09-12 08:30:53 +09:00
lockee14
b84f1173b7 add a comma at the end of line 114 2019-09-12 08:15:42 +09:00
lockee14
bdfe8c0445 add a comma at the end of line 73 2019-09-12 08:14:39 +09:00
lockee14
f64d0b35e1 Merge branch 'master' into Insert_Date_ISO_8601_Style 2019-08-30 21:54:38 +09:00
mehdi
928e0edf4d forgot to remove commented code 2019-07-29 16:24:42 +09:00
mehdi
80a63f7404 refactor: move the config down to editor setting 2019-07-08 19:27:23 +09:00
mehdi
6e45ee6a38 add Date ISO 8601 format 2019-06-24 06:10:31 +09:00
Luis Reinoso
8ec7d19f30 Remove unnecessary console.log 2019-03-12 22:38:10 -05:00
Luis Reinoso
a0f5a06c73 Add send wakatimeHeartBeat on constructor CodeEditor to fix when change from Markdown to Snippet #2810 2019-03-12 22:14:10 -05:00
Luis Reinoso
39a98e795f Add preferences plugins wakatime key #2810 2019-03-12 22:05:30 -05:00
Luis Reinoso
57705cf41b Add less WakatimeHeartBeat requests #2810 2019-03-12 20:40:27 -05:00
Luis Reinoso
052c70bb38 Add support to snippetNote #2810 2019-03-12 20:38:55 -05:00
Luis Reinoso
6dc88262c9 Add wakatime-plugin #2810 2019-03-12 20:02:24 -05:00
Baptiste Augrain
9d43e34cfa Merge branch 'master' into export-yfm 2018-12-28 00:02:38 +01:00
Baptiste Augrain
12f9b9342d Merge branch 'master' into fix-autocomplete-codeblock 2018-12-27 22:57:05 +01:00
Baptiste Augrain
e76bc72667 - data-line attributes might not be directly under the body
- support checkbox preference `When scrolling, synchronize preview with editor`
2018-12-24 11:44:02 +01:00
Baptiste Augrain
9310e5e86c Merge branch 'master' into fix-scroll 2018-12-24 10:33:47 +01:00
Baptiste Augrain
fa157f6f76 fix lint error 2018-12-15 15:57:37 +01:00
Baptiste Augrain
d6a54b8a26 export diagrams 2018-12-15 15:53:49 +01:00
Baptiste Augrain
9813412c8e add export menu in note list's context menu 2018-12-15 14:55:13 +01:00
Baptiste Augrain
d76b7235db export styles of code blocks 2018-12-15 12:42:39 +01:00
Baptiste Augrain
418a789568 fix export note in with split editor 2018-12-15 10:22:02 +01:00
Baptiste Augrain
2d941c3ea3 Merge branch 'master' into export-yfm 2018-12-13 14:35:25 +01:00
Baptiste Augrain
540c608cc6 Merge branch 'master' into fix-scroll 2018-11-28 14:01:18 +01:00
Baptiste Augrain
e723d4cd59 add tests 2018-11-18 19:10:48 +01:00
Baptiste Augrain
9e770ef357 fix bad conditions 2018-11-15 23:52:42 +01:00
Baptiste Augrain
c796b3b30e add YAML front matter when exporting 2018-11-15 22:48:14 +01:00
Baptiste Augrain
168fe212f5 add preference tab 2018-11-15 03:15:11 +01:00
Baptiste Augrain
87515dbd3f separate autocomplete configuration between markdown and within code blocks 2018-11-10 12:20:51 +01:00
Baptiste Augrain
696c2f29b5 implements better positioning between the editor and the preview in the SplitEditor (particularly with lot of images) 2018-09-17 16:59:27 +02:00
54 changed files with 3226 additions and 986 deletions

View File

@@ -21,6 +21,8 @@ const buildEditorContextMenu = require('browser/lib/contextMenuBuilder')
import { createTurndownService } from '../lib/turndown' import { createTurndownService } from '../lib/turndown'
import { languageMaps } from '../lib/CMLanguageList' import { languageMaps } from '../lib/CMLanguageList'
import snippetManager from '../lib/SnippetManager' import snippetManager from '../lib/SnippetManager'
import { findStorage } from 'browser/lib/findStorage'
import { sendWakatimeHeartBeat } from 'browser/lib/wakatime-plugin'
import { import {
generateInEditor, generateInEditor,
tocExistsInEditor tocExistsInEditor
@@ -61,7 +63,7 @@ export default class CodeEditor extends React.Component {
this.focusHandler = () => { this.focusHandler = () => {
ipcRenderer.send('editor:focused', true) ipcRenderer.send('editor:focused', true)
} }
const debouncedDeletionOfAttachments = _.debounce( this.debouncedDeletionOfAttachments = _.debounce(
attachmentManagement.deleteAttachmentsNotPresentInNote, attachmentManagement.deleteAttachmentsNotPresentInNote,
30000 30000
) )
@@ -78,7 +80,7 @@ export default class CodeEditor extends React.Component {
this.props.onBlur != null && this.props.onBlur(e) this.props.onBlur != null && this.props.onBlur(e)
const { storageKey, noteKey } = this.props const { storageKey, noteKey } = this.props
if (this.props.deleteUnusedAttachments === true) { if (this.props.deleteUnusedAttachments === true) {
debouncedDeletionOfAttachments( this.debouncedDeletionOfAttachments(
this.editor.getValue(), this.editor.getValue(),
storageKey, storageKey,
noteKey noteKey
@@ -113,6 +115,16 @@ export default class CodeEditor extends React.Component {
this.editorActivityHandler = () => this.handleEditorActivity() this.editorActivityHandler = () => this.handleEditorActivity()
this.turndownService = createTurndownService() 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) { handleSearch(msg) {
@@ -158,6 +170,10 @@ export default class CodeEditor extends React.Component {
} }
handleEditorActivity() { handleEditorActivity() {
if (this.props.onCursorActivity) {
this.props.onCursorActivity(this.editor)
}
if (!this.textEditorInterface.transaction) { if (!this.textEditorInterface.transaction) {
this.updateTableEditorState() this.updateTableEditorState()
} }
@@ -219,11 +235,19 @@ export default class CodeEditor extends React.Component {
}, },
[translateHotkey(hotkey.insertDate)]: function(cm) { [translateHotkey(hotkey.insertDate)]: function(cm) {
const dateNow = new Date() 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) { [translateHotkey(hotkey.insertDateTime)]: function(cm) {
const dateNow = new Date() const dateNow = new Date()
cm.replaceSelection(dateNow.toLocaleString()) if (self.props.dateFormatISO8601) {
cm.replaceSelection(dateNow.toISOString())
} else {
cm.replaceSelection(dateNow.toLocaleString())
}
}, },
Enter: 'boostNewLineAndIndentContinueMarkdownList', Enter: 'boostNewLineAndIndentContinueMarkdownList',
'Ctrl-C': cm => { 'Ctrl-C': cm => {
@@ -321,10 +345,18 @@ export default class CodeEditor extends React.Component {
'CodeMirror-lint-markers' 'CodeMirror-lint-markers'
], ],
autoCloseBrackets: { autoCloseBrackets: {
pairs: this.props.matchingPairs, codeBlock: {
triples: this.props.matchingTriples, pairs: this.props.codeBlockMatchingPairs,
explode: this.props.explodingPairs, closeBefore: this.props.codeBlockMatchingCloseBefore,
override: true 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, extraKeys: this.defaultKeyMap,
prettierConfig: this.props.prettierConfig prettierConfig: this.props.prettierConfig
@@ -352,6 +384,7 @@ export default class CodeEditor extends React.Component {
eventEmitter.emit('code:init') eventEmitter.emit('code:init')
this.editor.on('scroll', this.scrollHandler) this.editor.on('scroll', this.scrollHandler)
this.editor.on('cursorActivity', this.editorActivityHandler)
const editorTheme = document.getElementById('editorTheme') const editorTheme = document.getElementById('editorTheme')
editorTheme.addEventListener('load', this.loadStyleHandler) editorTheme.addEventListener('load', this.loadStyleHandler)
@@ -489,7 +522,6 @@ export default class CodeEditor extends React.Component {
}) })
if (this.props.enableTableEditor) { if (this.props.enableTableEditor) {
this.editor.on('cursorActivity', this.editorActivityHandler)
this.editor.on('changes', 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) this.editor.off('paste', this.pasteHandler)
eventEmitter.off('top:search', this.searchHandler) eventEmitter.off('top:search', this.searchHandler)
this.editor.off('scroll', this.scrollHandler) this.editor.off('scroll', this.scrollHandler)
this.editor.off('cursorActivity', this.editorActivityHandler)
this.editor.off('contextmenu', this.contextMenuHandler) this.editor.off('contextmenu', this.contextMenuHandler)
const editorTheme = document.getElementById('editorTheme') const editorTheme = document.getElementById('editorTheme')
editorTheme.removeEventListener('load', this.loadStyleHandler) editorTheme.removeEventListener('load', this.loadStyleHandler)
spellcheck.setLanguage(null, spellcheck.SPELLCHECK_DISABLED) spellcheck.setLanguage(null, spellcheck.SPELLCHECK_DISABLED)
eventEmitter.off('code:format-table', this.formatTable) eventEmitter.off('code:format-table', this.formatTable)
if (this.props.enableTableEditor) {
this.editor.off('changes', this.editorActivityHandler)
}
} }
componentDidUpdate(prevProps, prevState) { componentDidUpdate(prevProps, prevState) {
@@ -629,16 +667,32 @@ export default class CodeEditor extends React.Component {
if ( if (
prevProps.matchingPairs !== this.props.matchingPairs || prevProps.matchingPairs !== this.props.matchingPairs ||
prevProps.matchingCloseBefore !== this.props.matchingCloseBefore ||
prevProps.matchingTriples !== this.props.matchingTriples || 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 = { const autoCloseBrackets = {
pairs: this.props.matchingPairs, codeBlock: {
triples: this.props.matchingTriples, pairs: this.props.codeBlockMatchingPairs,
explode: this.props.explodingPairs, closeBefore: this.props.codeBlockMatchingCloseBefore,
override: true 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) { if (prevProps.enableTableEditor !== this.props.enableTableEditor) {
@@ -756,6 +810,8 @@ export default class CodeEditor extends React.Component {
} }
handleChange(editor, changeObject) { handleChange(editor, changeObject) {
this.debouncedDeletionOfAttachments.cancel()
spellcheck.handleChange(editor, changeObject) spellcheck.handleChange(editor, changeObject)
// The current note contains an toc. We'll check for changes on headlines. // 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.updateHighlight(editor, changeObject)
this.value = editor.getValue() this.value = editor.getValue()
const { storageKey, noteKey } = this.props
const storage = findStorage(storageKey)
if (this.props.onChange) { if (this.props.onChange) {
this.props.onChange(editor) 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) { linePossibleContainsHeadline(currentLine) {
@@ -923,6 +993,16 @@ export default class CodeEditor extends React.Component {
this.restartHighlighting() this.restartHighlighting()
this.editor.on('change', this.changeHandler) this.editor.on('change', this.changeHandler)
this.editor.refresh() 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) { setValue(value) {

View File

@@ -139,7 +139,7 @@ class MarkdownEditor extends React.Component {
}, },
() => { () => {
this.previewRef.current.focus() this.previewRef.current.focus()
this.previewRef.current.scrollToRow(cursorPosition.line) this.previewRef.current.scrollToLine(cursorPosition.line)
} }
) )
eventEmitter.emit('topbar:togglelockbutton', this.state.status) eventEmitter.emit('topbar:togglelockbutton', this.state.status)
@@ -323,6 +323,7 @@ class MarkdownEditor extends React.Component {
storageKey, storageKey,
noteKey, noteKey,
linesHighlighted, linesHighlighted,
getNote,
RTL RTL
} = this.props } = this.props
@@ -365,8 +366,15 @@ class MarkdownEditor extends React.Component {
displayLineNumbers={config.editor.displayLineNumbers} displayLineNumbers={config.editor.displayLineNumbers}
lineWrapping lineWrapping
matchingPairs={config.editor.matchingPairs} matchingPairs={config.editor.matchingPairs}
matchingCloseBefore={config.editor.matchingCloseBefore}
matchingTriples={config.editor.matchingTriples} matchingTriples={config.editor.matchingTriples}
explodingPairs={config.editor.explodingPairs} explodingPairs={config.editor.explodingPairs}
codeBlockMatchingPairs={config.editor.codeBlockMatchingPairs}
codeBlockMatchingCloseBefore={
config.editor.codeBlockMatchingCloseBefore
}
codeBlockMatchingTriples={config.editor.codeBlockMatchingTriples}
codeBlockExplodingPairs={config.editor.codeBlockExplodingPairs}
scrollPastEnd={config.editor.scrollPastEnd} scrollPastEnd={config.editor.scrollPastEnd}
storageKey={storageKey} storageKey={storageKey}
noteKey={noteKey} noteKey={noteKey}
@@ -381,6 +389,7 @@ class MarkdownEditor extends React.Component {
switchPreview={config.editor.switchPreview} switchPreview={config.editor.switchPreview}
enableMarkdownLint={config.editor.enableMarkdownLint} enableMarkdownLint={config.editor.enableMarkdownLint}
customMarkdownLintConfig={config.editor.customMarkdownLintConfig} customMarkdownLintConfig={config.editor.customMarkdownLintConfig}
dateFormatISO8601={config.editor.dateFormatISO8601}
prettierConfig={config.editor.prettierConfig} prettierConfig={config.editor.prettierConfig}
deleteUnusedAttachments={config.editor.deleteUnusedAttachments} deleteUnusedAttachments={config.editor.deleteUnusedAttachments}
RTL={RTL} RTL={RTL}
@@ -418,6 +427,8 @@ class MarkdownEditor extends React.Component {
customCSS={config.preview.customCSS} customCSS={config.preview.customCSS}
allowCustomCSS={config.preview.allowCustomCSS} allowCustomCSS={config.preview.allowCustomCSS}
lineThroughCheckbox={config.preview.lineThroughCheckbox} lineThroughCheckbox={config.preview.lineThroughCheckbox}
getNote={getNote}
export={config.export}
onDrop={e => this.handleDropImage(e)} onDrop={e => this.handleDropImage(e)}
RTL={RTL} RTL={RTL}
/> />

View File

@@ -18,258 +18,30 @@ import convertModeName from 'browser/lib/convertModeName'
import copy from 'copy-to-clipboard' import copy from 'copy-to-clipboard'
import mdurl from 'mdurl' import mdurl from 'mdurl'
import exportNote from 'browser/main/lib/dataApi/exportNote' import exportNote from 'browser/main/lib/dataApi/exportNote'
import { escapeHtmlCharacters } from 'browser/lib/utils' import formatMarkdown from 'browser/main/lib/dataApi/formatMarkdown'
import formatHTML, {
CSS_FILES,
buildStyle,
getCodeThemeLink,
getStyleParams,
escapeHtmlCharactersInCodeTag
} from 'browser/main/lib/dataApi/formatHTML'
import formatPDF from 'browser/main/lib/dataApi/formatPDF'
import yaml from 'js-yaml' import yaml from 'js-yaml'
import i18n from 'browser/lib/i18n'
import path from 'path'
import { remote, shell } from 'electron'
import attachmentManagement from '../main/lib/dataApi/attachmentManagement'
import filenamify from 'filenamify'
import { render } from 'react-dom' import { render } from 'react-dom'
import Carousel from 'react-image-carousel' import Carousel from 'react-image-carousel'
import { push } from 'connected-react-router' import { push } from 'connected-react-router'
import ConfigManager from '../main/lib/ConfigManager' import ConfigManager from '../main/lib/ConfigManager'
import uiThemes from 'browser/lib/ui-themes' import uiThemes from 'browser/lib/ui-themes'
import i18n from 'browser/lib/i18n' import { buildMarkdownPreviewContextMenu } from 'browser/lib/contextMenuBuilder'
const { remote, shell } = require('electron')
const attachmentManagement = require('../main/lib/dataApi/attachmentManagement')
const buildMarkdownPreviewContextMenu = require('browser/lib/contextMenuBuilder')
.buildMarkdownPreviewContextMenu
const { app } = remote
const path = require('path')
const fileUrl = require('file-url')
const dialog = remote.dialog const dialog = remote.dialog
const markdownStyle = require('!!css!stylus?sourceMap!./markdown.styl')[0][1]
const appPath = fileUrl(
process.env.NODE_ENV === 'production' ? app.getAppPath() : path.resolve()
)
const CSS_FILES = [
`${appPath}/node_modules/katex/dist/katex.min.css`,
`${appPath}/node_modules/codemirror/lib/codemirror.css`,
`${appPath}/node_modules/react-image-carousel/lib/css/main.min.css`
]
/**
* @param {Object} opts
* @param {String} opts.fontFamily
* @param {Numberl} opts.fontSize
* @param {String} opts.codeBlockFontFamily
* @param {String} opts.theme
* @param {Boolean} [opts.lineNumber] Should show line number
* @param {Boolean} [opts.scrollPastEnd]
* @param {Boolean} [opts.allowCustomCSS] Should add custom css
* @param {String} [opts.customCSS] Will be added to bottom, only if `opts.allowCustomCSS` is truthy
* @returns {String}
*/
function buildStyle(opts) {
const {
fontFamily,
fontSize,
codeBlockFontFamily,
lineNumber,
scrollPastEnd,
theme,
allowCustomCSS,
customCSS,
RTL
} = opts
return `
@font-face {
font-family: 'Lato';
src: url('${appPath}/resources/fonts/Lato-Regular.woff2') format('woff2'), /* Modern Browsers */
url('${appPath}/resources/fonts/Lato-Regular.woff') format('woff'), /* Modern Browsers */
url('${appPath}/resources/fonts/Lato-Regular.ttf') format('truetype');
font-style: normal;
font-weight: normal;
text-rendering: optimizeLegibility;
}
@font-face {
font-family: 'Lato';
src: url('${appPath}/resources/fonts/Lato-Black.woff2') format('woff2'), /* Modern Browsers */
url('${appPath}/resources/fonts/Lato-Black.woff') format('woff'), /* Modern Browsers */
url('${appPath}/resources/fonts/Lato-Black.ttf') format('truetype');
font-style: normal;
font-weight: 700;
text-rendering: optimizeLegibility;
}
@font-face {
font-family: 'Material Icons';
font-style: normal;
font-weight: 400;
src: local('Material Icons'),
local('MaterialIcons-Regular'),
url('${appPath}/resources/fonts/MaterialIcons-Regular.woff2') format('woff2'),
url('${appPath}/resources/fonts/MaterialIcons-Regular.woff') format('woff'),
url('${appPath}/resources/fonts/MaterialIcons-Regular.ttf') format('truetype');
}
${markdownStyle}
body {
font-family: '${fontFamily.join("','")}';
font-size: ${fontSize}px;
${
scrollPastEnd
? `
padding-bottom: 90vh;
box-sizing: border-box;
`
: ''
}
${RTL ? 'direction: rtl;' : ''}
${RTL ? 'text-align: right;' : ''}
}
@media print {
body {
padding-bottom: initial;
}
}
code {
font-family: '${codeBlockFontFamily.join("','")}';
background-color: rgba(0,0,0,0.04);
text-align: left;
direction: ltr;
}
p code,
li code,
td code
{
padding: 2px;
border-width: 1px;
border-style: solid;
border-radius: 5px;
}
[data-theme="default"] p code,
[data-theme="default"] li code,
[data-theme="default"] td code
{
background-color: #F4F4F4;
border-color: #d9d9d9;
color: inherit;
}
[data-theme="white"] p code,
[data-theme="white"] li code,
[data-theme="white"] td code
{
background-color: #F4F4F4;
border-color: #d9d9d9;
color: inherit;
}
[data-theme="dark"] p code,
[data-theme="dark"] li code,
[data-theme="dark"] td code
{
background-color: #444444;
border-color: #555;
color: #FFFFFF;
}
[data-theme="dracula"] p code,
[data-theme="dracula"] li code,
[data-theme="dracula"] td code
{
background-color: #444444;
border-color: #555;
color: #FFFFFF;
}
[data-theme="monokai"] p code,
[data-theme="monokai"] li code,
[data-theme="monokai"] td code
{
background-color: #444444;
border-color: #555;
color: #FFFFFF;
}
[data-theme="nord"] p code,
[data-theme="nord"] li code,
[data-theme="nord"] td code
{
background-color: #444444;
border-color: #555;
color: #FFFFFF;
}
[data-theme="solarized-dark"] p code,
[data-theme="solarized-dark"] li code,
[data-theme="solarized-dark"] td code
{
background-color: #444444;
border-color: #555;
color: #FFFFFF;
}
[data-theme="vulcan"] p code,
[data-theme="vulcan"] li code,
[data-theme="vulcan"] td code
{
background-color: #444444;
border-color: #555;
color: #FFFFFF;
}
.lineNumber {
${lineNumber && 'display: block !important;'}
font-family: '${codeBlockFontFamily.join("','")}';
}
.clipboardButton {
color: rgba(147,147,149,0.8);;
fill: rgba(147,147,149,1);;
border-radius: 50%;
margin: 0px 10px;
border: none;
background-color: transparent;
outline: none;
height: 15px;
width: 15px;
cursor: pointer;
}
.clipboardButton:hover {
transition: 0.2s;
color: #939395;
fill: #939395;
background-color: rgba(0,0,0,0.1);
}
h1, h2 {
border: none;
}
h3 {
margin: 1em 0 0.8em;
}
h4, h5, h6 {
margin: 1.1em 0 0.5em;
}
h1 {
padding: 0.2em 0 0.2em;
margin: 1em 0 8px;
}
h2 {
padding: 0.2em 0 0.2em;
margin: 1em 0 0.7em;
}
body p {
white-space: normal;
}
@media print {
body[data-theme="${theme}"] {
color: #000;
background-color: #fff;
}
.clipboardButton {
display: none
}
}
${allowCustomCSS ? customCSS : ''}
`
}
const scrollBarStyle = ` const scrollBarStyle = `
::-webkit-scrollbar { ::-webkit-scrollbar {
${config.get().ui.showScrollBar ? '' : 'display: none;'} ${config.get().ui.showScrollBar ? '' : 'display: none;'}
@@ -301,22 +73,6 @@ const scrollBarDarkStyle = `
} }
` `
const OSX = global.process.platform === 'darwin'
const defaultFontFamily = ['helvetica', 'arial', 'sans-serif']
if (!OSX) {
defaultFontFamily.unshift('Microsoft YaHei')
defaultFontFamily.unshift('meiryo')
}
const defaultCodeBlockFontFamily = [
'Monaco',
'Menlo',
'Ubuntu Mono',
'Consolas',
'source-code-pro',
'monospace'
]
// return the line number of the line that used to generate the specified element // return the line number of the line that used to generate the specified element
// return -1 if the line is not found // return -1 if the line is not found
function getSourceLineNumberByElement(element) { function getSourceLineNumberByElement(element) {
@@ -430,94 +186,15 @@ class MarkdownPreview extends React.Component {
} }
handleSaveAsMd() { handleSaveAsMd() {
this.exportAsDocument('md') this.exportAsDocument('md', formatMarkdown(this.props))
}
htmlContentFormatter(noteContent, exportTasks, targetDir) {
const {
fontFamily,
fontSize,
codeBlockFontFamily,
lineNumber,
codeBlockTheme,
scrollPastEnd,
theme,
allowCustomCSS,
customCSS,
RTL
} = this.getStyleParams()
const inlineStyles = buildStyle({
fontFamily,
fontSize,
codeBlockFontFamily,
lineNumber,
scrollPastEnd,
theme,
allowCustomCSS,
customCSS,
RTL
})
let body = this.refs.root.contentWindow.document.body.innerHTML
body = attachmentManagement.fixLocalURLS(body, this.props.storagePath)
const files = [this.getCodeThemeLink(codeBlockTheme), ...CSS_FILES]
files.forEach(file => {
if (global.process.platform === 'win32') {
file = file.replace('file:///', '')
} else {
file = file.replace('file://', '')
}
exportTasks.push({
src: file,
dst: 'css'
})
})
let styles = ''
files.forEach(file => {
styles += `<link rel="stylesheet" href="../css/${path.basename(file)}">`
})
return `<html>
<head>
<base href="file://${targetDir}/">
<meta charset="UTF-8">
<meta name = "viewport" content = "width = device-width, initial-scale = 1, maximum-scale = 1">
<style id="style">${inlineStyles}</style>
${styles}
</head>
<body>${body}</body>
</html>`
} }
handleSaveAsHtml() { handleSaveAsHtml() {
this.exportAsDocument('html', (noteContent, exportTasks, targetDir) => this.exportAsDocument('html', formatHTML(this.props))
Promise.resolve(
this.htmlContentFormatter(noteContent, exportTasks, targetDir)
)
)
} }
handleSaveAsPdf() { handleSaveAsPdf() {
this.exportAsDocument('pdf', (noteContent, exportTasks, targetDir) => { this.exportAsDocument('pdf', formatPDF(this.props))
const printout = new remote.BrowserWindow({
show: false,
webPreferences: { webSecurity: false, javascript: false }
})
printout.loadURL(
'data:text/html;charset=UTF-8,' +
this.htmlContentFormatter(noteContent, exportTasks, targetDir)
)
return new Promise((resolve, reject) => {
printout.webContents.on('did-finish-load', () => {
printout.webContents.printToPDF({}, (err, data) => {
if (err) reject(err)
else resolve(data)
printout.destroy()
})
})
})
})
} }
handlePrint() { handlePrint() {
@@ -525,18 +202,21 @@ class MarkdownPreview extends React.Component {
} }
exportAsDocument(fileType, contentFormatter) { exportAsDocument(fileType, contentFormatter) {
const note = this.props.getNote()
const options = { const options = {
defaultPath: filenamify(note.title, {
replacement: '_'
}),
filters: [{ name: 'Documents', extensions: [fileType] }], filters: [{ name: 'Documents', extensions: [fileType] }],
properties: ['openFile', 'createDirectory'] properties: ['openFile', 'createDirectory']
} }
dialog.showSaveDialog(remote.getCurrentWindow(), options, filename => { dialog.showSaveDialog(remote.getCurrentWindow(), options, filename => {
if (filename) { if (filename) {
const content = this.props.value const storagePath = this.props.storagePath
const storage = this.props.storagePath
const nodeKey = this.props.noteKey
exportNote(nodeKey, storage, content, filename, contentFormatter) exportNote(storagePath, note, filename, contentFormatter)
.then(res => { .then(res => {
dialog.showMessageBox(remote.getCurrentWindow(), { dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'info', type: 'info',
@@ -567,32 +247,6 @@ class MarkdownPreview extends React.Component {
} }
} }
/**
* @description Convert special characters between three ```
* @param {string[]} splitWithCodeTag Array of HTML strings separated by three ```
* @returns {string} HTML in which special characters between three ``` have been converted
*/
escapeHtmlCharactersInCodeTag(splitWithCodeTag) {
for (let index = 0; index < splitWithCodeTag.length; index++) {
const codeTagRequired =
splitWithCodeTag[index] !== '```' && index < splitWithCodeTag.length - 1
if (codeTagRequired) {
splitWithCodeTag.splice(index + 1, 0, '```')
}
}
let inCodeTag = false
let result = ''
for (let content of splitWithCodeTag) {
if (content === '```') {
inCodeTag = !inCodeTag
} else if (inCodeTag) {
content = escapeHtmlCharacters(content)
}
result += content
}
return result
}
getScrollBarStyle() { getScrollBarStyle() {
const { theme } = this.props const { theme } = this.props
@@ -743,47 +397,6 @@ class MarkdownPreview extends React.Component {
} }
} }
getStyleParams() {
const {
fontSize,
lineNumber,
codeBlockTheme,
scrollPastEnd,
theme,
allowCustomCSS,
customCSS,
RTL
} = this.props
let { fontFamily, codeBlockFontFamily } = this.props
fontFamily =
_.isString(fontFamily) && fontFamily.trim().length > 0
? fontFamily
.split(',')
.map(fontName => fontName.trim())
.concat(defaultFontFamily)
: defaultFontFamily
codeBlockFontFamily =
_.isString(codeBlockFontFamily) && codeBlockFontFamily.trim().length > 0
? codeBlockFontFamily
.split(',')
.map(fontName => fontName.trim())
.concat(defaultCodeBlockFontFamily)
: defaultCodeBlockFontFamily
return {
fontFamily,
fontSize,
codeBlockFontFamily,
lineNumber,
codeBlockTheme,
scrollPastEnd,
theme,
allowCustomCSS,
customCSS,
RTL
}
}
applyStyle() { applyStyle() {
const { const {
fontFamily, fontFamily,
@@ -796,12 +409,13 @@ class MarkdownPreview extends React.Component {
allowCustomCSS, allowCustomCSS,
customCSS, customCSS,
RTL RTL
} = this.getStyleParams() } = getStyleParams(this.props)
this.getWindow().document.getElementById( this.getWindow().document.getElementById(
'codeTheme' 'codeTheme'
).href = this.getCodeThemeLink(codeBlockTheme) ).href = getCodeThemeLink(codeBlockTheme)
this.getWindow().document.getElementById('style').innerHTML = buildStyle({
this.getWindow().document.getElementById('style').innerHTML = buildStyle(
fontFamily, fontFamily,
fontSize, fontSize,
codeBlockFontFamily, codeBlockFontFamily,
@@ -811,15 +425,7 @@ class MarkdownPreview extends React.Component {
allowCustomCSS, allowCustomCSS,
customCSS, customCSS,
RTL RTL
}) )
}
getCodeThemeLink(name) {
const theme = consts.THEMES.find(theme => theme.name === name)
return theme != null
? theme.path
: `${appPath}/node_modules/codemirror/theme/elegant.css`
} }
rewriteIframe() { rewriteIframe() {
@@ -853,7 +459,7 @@ class MarkdownPreview extends React.Component {
this.refs.root.contentWindow.document.body.setAttribute('data-theme', theme) this.refs.root.contentWindow.document.body.setAttribute('data-theme', theme)
if (sanitize === 'NONE') { if (sanitize === 'NONE') {
const splitWithCodeTag = value.split('```') const splitWithCodeTag = value.split('```')
value = this.escapeHtmlCharactersInCodeTag(splitWithCodeTag) value = escapeHtmlCharactersInCodeTag(splitWithCodeTag)
} }
const renderedHTML = this.markdown.render(value) const renderedHTML = this.markdown.render(value)
attachmentManagement.migrateAttachments(value, storagePath, noteKey) attachmentManagement.migrateAttachments(value, storagePath, noteKey)
@@ -916,13 +522,9 @@ class MarkdownPreview extends React.Component {
}) })
} }
) )
const opts = {} const opts = {}
// if (this.props.theme === 'dark') {
// opts['font-color'] = '#DDD'
// opts['line-color'] = '#DDD'
// opts['element-color'] = '#DDD'
// opts['fill'] = '#3A404C'
// }
_.forEach( _.forEach(
this.refs.root.contentWindow.document.querySelectorAll('.flowchart'), this.refs.root.contentWindow.document.querySelectorAll('.flowchart'),
el => { el => {
@@ -1145,17 +747,18 @@ class MarkdownPreview extends React.Component {
/** /**
* @public * @public
* @param {Number} targetRow * @param {Number} targetLine
*/ */
scrollToRow(targetRow) { scrollToLine(targetLine) {
const blocks = this.getWindow().document.querySelectorAll( const blocks = this.getWindow().document.querySelectorAll(
'body>[data-line]' 'body [data-line]'
) )
for (let index = 0; index < blocks.length; index++) { for (let index = 0; index < blocks.length; index++) {
let block = blocks[index] let block = blocks[index]
const row = parseInt(block.getAttribute('data-line')) const line = parseInt(block.getAttribute('data-line'))
if (row > targetRow || index === blocks.length - 1) {
if (line > targetLine || index === blocks.length - 1) {
block = blocks[index - 1] block = blocks[index - 1]
block != null && this.scrollTo(0, block.offsetTop) block != null && this.scrollTo(0, block.offsetTop)
break break
@@ -1192,7 +795,10 @@ class MarkdownPreview extends React.Component {
e.preventDefault() e.preventDefault()
e.stopPropagation() 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 const { dispatch } = this.props
if (!rawHref) return // not checked href because parser will create file://... string for [empty link]() if (!rawHref) return // not checked href because parser will create file://... string for [empty link]()

View File

@@ -13,7 +13,7 @@ class MarkdownSplitEditor extends React.Component {
this.value = props.value this.value = props.value
this.focus = () => this.refs.code.focus() this.focus = () => this.refs.code.focus()
this.reload = () => this.refs.code.reload() this.reload = () => this.refs.code.reload()
this.userScroll = true this.userScroll = props.config.preview.scrollSync
this.state = { this.state = {
isSliderFocused: false, isSliderFocused: false,
codeEditorWidthInPercent: 50, codeEditorWidthInPercent: 50,
@@ -21,6 +21,72 @@ class MarkdownSplitEditor extends React.Component {
} }
} }
componentDidUpdate(prevProps) {
if (
this.props.config.preview.scrollSync !==
prevProps.config.preview.scrollSync
) {
this.userScroll = this.props.config.preview.scrollSync
}
}
handleCursorActivity(editor) {
if (this.userScroll) {
const previewDoc = _.get(
this,
'refs.preview.refs.root.contentWindow.document'
)
const previewTop = _.get(previewDoc, 'body.scrollTop')
const line = editor.doc.getCursor().line
let top
if (line === 0) {
top = 0
} else {
const blockElements = previewDoc.querySelectorAll('body [data-line]')
const blocks = []
for (const block of blockElements) {
const l = parseInt(block.getAttribute('data-line'))
blocks.push({
line: l,
top: block.offsetTop
})
if (l > line) {
break
}
}
if (blocks.length === 1) {
const block = blockElements[blockElements.length - 1]
blocks.push({
line: editor.doc.size,
top: block.offsetTop + block.offsetHeight
})
}
const i = blocks.length - 1
const ratio =
(blocks[i].top - blocks[i - 1].top) /
(blocks[i].line - blocks[i - 1].line)
const delta = Math.floor(_.get(previewDoc, 'body.clientHeight') / 3)
top =
blocks[i - 1].top +
Math.floor((line - blocks[i - 1].line) * ratio) -
delta
}
this.scrollTo(previewTop, top, y =>
_.set(previewDoc, 'body.scrollTop', y)
)
}
}
setValue(value) { setValue(value) {
this.refs.code.setValue(value) this.refs.code.setValue(value)
} }
@@ -30,59 +96,125 @@ class MarkdownSplitEditor extends React.Component {
this.props.onChange(e) this.props.onChange(e)
} }
handleScroll(e) { handleEditorScroll(e) {
if (!this.props.config.preview.scrollSync) return
const previewDoc = _.get(
this,
'refs.preview.refs.root.contentWindow.document'
)
const codeDoc = _.get(this, 'refs.code.editor.doc')
let srcTop, srcHeight, targetTop, targetHeight
if (this.userScroll) { if (this.userScroll) {
if (e.doc) { const previewDoc = _.get(
srcTop = _.get(e, 'doc.scrollTop') this,
srcHeight = _.get(e, 'doc.height') 'refs.preview.refs.root.contentWindow.document'
targetTop = _.get(previewDoc, 'body.scrollTop') )
targetHeight = _.get(previewDoc, 'body.scrollHeight') 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 { } else {
srcTop = _.get(previewDoc, 'body.scrollTop') const line = from + Math.floor((to - from) / 3)
srcHeight = _.get(previewDoc, 'body.scrollHeight')
targetTop = _.get(codeDoc, 'scrollTop') const blockElements = previewDoc.querySelectorAll('body [data-line]')
targetHeight = _.get(codeDoc, 'height') 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 this.scrollTo(previewTop, top, y =>
const framerate = 1000 / 60 _.set(previewDoc, 'body.scrollTop', y)
const frames = 20 )
const refractory = frames * framerate }
}
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 const srcTop = _.get(previewDoc, 'body.scrollTop')
let scrollPos, time const editorTop = _.get(codeDoc, 'scrollTop')
const timer = setInterval(() => {
time = frame / frames let top
scrollPos = if (srcTop === 0) {
time < 0.5 top = 0
? 2 * time * time // ease in } else {
: -1 + (4 - 2 * time) * time // ease out const delta = Math.floor(_.get(previewDoc, 'body.clientHeight') / 3)
if (e.doc) const previewTop = srcTop + delta
_.set(previewDoc, 'body.scrollTop', targetTop + scrollPos * distance)
else const blockElements = previewDoc.querySelectorAll('body [data-line]')
_.get(this, 'refs.code.editor').scrollTo( const blocks = []
0, for (const block of blockElements) {
targetTop + scrollPos * distance const top = block.offsetTop
)
if (frame >= frames) { blocks.push({
clearInterval(timer) line: parseInt(block.getAttribute('data-line')),
setTimeout(() => { top
this.userScroll = true })
}, refractory)
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))
} }
} }
@@ -168,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() { render() {
const { const {
config, config,
@@ -175,6 +336,7 @@ class MarkdownSplitEditor extends React.Component {
storageKey, storageKey,
noteKey, noteKey,
linesHighlighted, linesHighlighted,
getNote,
isStacking, isStacking,
RTL RTL
} = this.props } = this.props
@@ -260,8 +422,15 @@ class MarkdownSplitEditor extends React.Component {
displayLineNumbers={config.editor.displayLineNumbers} displayLineNumbers={config.editor.displayLineNumbers}
lineWrapping lineWrapping
matchingPairs={config.editor.matchingPairs} matchingPairs={config.editor.matchingPairs}
matchingCloseBefore={config.editor.matchingCloseBefore}
matchingTriples={config.editor.matchingTriples} matchingTriples={config.editor.matchingTriples}
explodingPairs={config.editor.explodingPairs} explodingPairs={config.editor.explodingPairs}
codeBlockMatchingPairs={config.editor.codeBlockMatchingPairs}
codeBlockMatchingCloseBefore={
config.editor.codeBlockMatchingCloseBefore
}
codeBlockMatchingTriples={config.editor.codeBlockMatchingTriples}
codeBlockExplodingPairs={config.editor.codeBlockExplodingPairs}
indentType={config.editor.indentType} indentType={config.editor.indentType}
indentSize={editorStyle.indentSize} indentSize={editorStyle.indentSize}
enableRulers={config.editor.enableRulers} enableRulers={config.editor.enableRulers}
@@ -273,13 +442,15 @@ class MarkdownSplitEditor extends React.Component {
noteKey={noteKey} noteKey={noteKey}
linesHighlighted={linesHighlighted} linesHighlighted={linesHighlighted}
onChange={e => this.handleOnChange(e)} onChange={e => this.handleOnChange(e)}
onScroll={this.handleScroll.bind(this)} onScroll={e => this.handleEditorScroll(e)}
onCursorActivity={e => this.handleCursorActivity(e)}
spellCheck={config.editor.spellcheck} spellCheck={config.editor.spellcheck}
enableSmartPaste={config.editor.enableSmartPaste} enableSmartPaste={config.editor.enableSmartPaste}
hotkey={config.hotkey} hotkey={config.hotkey}
switchPreview={config.editor.switchPreview} switchPreview={config.editor.switchPreview}
enableMarkdownLint={config.editor.enableMarkdownLint} enableMarkdownLint={config.editor.enableMarkdownLint}
customMarkdownLintConfig={config.editor.customMarkdownLintConfig} customMarkdownLintConfig={config.editor.customMarkdownLintConfig}
dateFormatISO8601={config.editor.dateFormatISO8601}
deleteUnusedAttachments={config.editor.deleteUnusedAttachments} deleteUnusedAttachments={config.editor.deleteUnusedAttachments}
RTL={RTL} RTL={RTL}
/> />
@@ -300,6 +471,7 @@ class MarkdownSplitEditor extends React.Component {
codeBlockTheme={config.preview.codeBlockTheme} codeBlockTheme={config.preview.codeBlockTheme}
codeBlockFontFamily={config.editor.fontFamily} codeBlockFontFamily={config.editor.fontFamily}
lineNumber={config.preview.lineNumber} lineNumber={config.preview.lineNumber}
indentSize={editorIndentSize}
scrollPastEnd={config.preview.scrollPastEnd} scrollPastEnd={config.preview.scrollPastEnd}
smartQuotes={config.preview.smartQuotes} smartQuotes={config.preview.smartQuotes}
smartArrows={config.preview.smartArrows} smartArrows={config.preview.smartArrows}
@@ -309,13 +481,15 @@ class MarkdownSplitEditor extends React.Component {
tabInde='0' tabInde='0'
value={value} value={value}
onCheckboxClick={e => this.handleCheckboxClick(e)} onCheckboxClick={e => this.handleCheckboxClick(e)}
onScroll={this.handleScroll.bind(this)} onScroll={e => this.handlePreviewScroll(e)}
showCopyNotification={config.ui.showCopyNotification} showCopyNotification={config.ui.showCopyNotification}
storagePath={storage.path} storagePath={storage.path}
noteKey={noteKey} noteKey={noteKey}
customCSS={config.preview.customCSS} customCSS={config.preview.customCSS}
allowCustomCSS={config.preview.allowCustomCSS} allowCustomCSS={config.preview.allowCustomCSS}
lineThroughCheckbox={config.preview.lineThroughCheckbox} lineThroughCheckbox={config.preview.lineThroughCheckbox}
getNote={getNote}
export={config.export}
RTL={RTL} RTL={RTL}
/> />
</div> </div>

View File

@@ -1,4 +1,4 @@
import mermaidAPI from 'mermaid' import mermaidAPI from 'mermaid/dist/mermaid.min.js'
import uiThemes from 'browser/lib/ui-themes' import uiThemes from 'browser/lib/ui-themes'
// fixes bad styling in the mermaid dark theme // fixes bad styling in the mermaid dark theme
@@ -61,7 +61,6 @@ function render(element, content, theme, enableHTMLLabel) {
el.setAttribute('ratio', ratio) el.setAttribute('ratio', ratio)
el.setAttribute('height', el.parentNode.clientWidth / ratio) el.setAttribute('height', el.parentNode.clientWidth / ratio)
console.log(el)
} }
}) })
} catch (e) { } catch (e) {

View File

@@ -8,7 +8,7 @@ import markdownItTocAndAnchor from '@hikerpig/markdown-it-toc-and-anchor'
import _ from 'lodash' import _ from 'lodash'
import ConfigManager from 'browser/main/lib/ConfigManager' import ConfigManager from 'browser/main/lib/ConfigManager'
import katex from 'katex' import katex from 'katex'
import { lastFindInArray } from './utils' import { escapeHtmlCharacters, lastFindInArray } from './utils'
function createGutter(str, firstLineNumber) { function createGutter(str, firstLineNumber) {
if (Number.isNaN(firstLineNumber)) firstLineNumber = 1 if (Number.isNaN(firstLineNumber)) firstLineNumber = 1
@@ -31,7 +31,8 @@ class Markdown {
html: true, html: true,
xhtmlOut: true, xhtmlOut: true,
breaks: config.preview.breaks, breaks: config.preview.breaks,
sanitize: 'STRICT' sanitize: 'STRICT',
onFence: () => {}
} }
const updatedOptions = Object.assign(defaultOptions, options) const updatedOptions = Object.assign(defaultOptions, options)
@@ -266,22 +267,26 @@ class Markdown {
token.parameters.format = 'yaml' token.parameters.format = 'yaml'
} }
updatedOptions.onFence('chart', token.parameters.format)
return `<pre class="fence" data-line="${token.map[0]}"> return `<pre class="fence" data-line="${token.map[0]}">
<span class="filename">${token.fileName}</span> <span class="filename">${token.fileName}</span>
<div class="chart" data-height="${ <div class="chart" data-height="${
token.parameters.height token.parameters.height
}" data-format="${token.parameters.format || 'json'}">${ }" data-format="${token.parameters.format || 'json'}">${
token.content token.content
}</div> }</div>
</pre>` </pre>`
}, },
flowchart: token => { flowchart: token => {
updatedOptions.onFence('flowchart')
return `<pre class="fence" data-line="${token.map[0]}"> return `<pre class="fence" data-line="${token.map[0]}">
<span class="filename">${token.fileName}</span> <span class="filename">${token.fileName}</span>
<div class="flowchart" data-height="${token.parameters.height}">${ <div class="flowchart" data-height="${token.parameters.height}">${
token.content token.content
}</div> }</div>
</pre>` </pre>`
}, },
gallery: token => { gallery: token => {
const content = token.content const content = token.content
@@ -298,35 +303,41 @@ class Markdown {
.join('\n') .join('\n')
return `<pre class="fence" data-line="${token.map[0]}"> return `<pre class="fence" data-line="${token.map[0]}">
<span class="filename">${token.fileName}</span> <span class="filename">${token.fileName}</span>
<div class="gallery" data-autoplay="${ <div class="gallery" data-autoplay="${
token.parameters.autoplay token.parameters.autoplay
}" data-height="${token.parameters.height}">${content}</div> }" data-height="${token.parameters.height}">${content}</div>
</pre>` </pre>`
}, },
mermaid: token => { mermaid: token => {
updatedOptions.onFence('mermaid')
return `<pre class="fence" data-line="${token.map[0]}"> return `<pre class="fence" data-line="${token.map[0]}">
<span class="filename">${token.fileName}</span> <span class="filename">${token.fileName}</span>
<div class="mermaid" data-height="${token.parameters.height}">${ <div class="mermaid" data-height="${token.parameters.height}">${
token.content token.content
}</div> }</div>
</pre>` </pre>`
}, },
sequence: token => { sequence: token => {
updatedOptions.onFence('sequence')
return `<pre class="fence" data-line="${token.map[0]}"> return `<pre class="fence" data-line="${token.map[0]}">
<span class="filename">${token.fileName}</span> <span class="filename">${token.fileName}</span>
<div class="sequence" data-height="${token.parameters.height}">${ <div class="sequence" data-height="${token.parameters.height}">${
token.content token.content
}</div> }</div>
</pre>` </pre>`
} }
}, },
token => { token => {
updatedOptions.onFence('code', token.langType)
return `<pre class="code CodeMirror" data-line="${token.map[0]}"> return `<pre class="code CodeMirror" data-line="${token.map[0]}">
<span class="filename">${token.fileName}</span> <span class="filename">${token.fileName}</span>
${createGutter(token.content, token.firstLineNumber)} ${createGutter(token.content, token.firstLineNumber)}
<code class="${token.langType}">${token.content}</code> <code class="${token.langType}">${token.content}</code>
</pre>` </pre>`
} }
) )
@@ -468,6 +479,16 @@ class Markdown {
return true 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) { if (config.preview.smartArrows) {
this.md.use(smartArrows) this.md.use(smartArrows)
} }

View 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 }

View File

@@ -57,10 +57,11 @@ class MarkdownNoteDetail extends React.Component {
this.dispatchTimer = null this.dispatchTimer = null
this.toggleLockButton = this.handleToggleLockButton.bind(this)
this.generateToc = this.handleGenerateToc.bind(this) this.generateToc = this.handleGenerateToc.bind(this)
this.toggleLockButton = this.handleToggleLockButton.bind(this)
this.handleUpdateContent = this.handleUpdateContent.bind(this) this.handleUpdateContent = this.handleUpdateContent.bind(this)
this.handleSwitchStackDirection = this.handleSwitchStackDirection.bind(this) this.handleSwitchStackDirection = this.handleSwitchStackDirection.bind(this)
this.getNote = this.getNote.bind(this)
} }
focus() { focus() {
@@ -441,6 +442,10 @@ class MarkdownNoteDetail extends React.Component {
this.updateNote(note) this.updateNote(note)
} }
getNote() {
return this.state.note
}
renderEditor() { renderEditor() {
const { config, ignorePreviewPointerEvents } = this.props const { config, ignorePreviewPointerEvents } = this.props
const { note, isStacking } = this.state const { note, isStacking } = this.state
@@ -456,8 +461,8 @@ class MarkdownNoteDetail extends React.Component {
noteKey={note.key} noteKey={note.key}
linesHighlighted={note.linesHighlighted} linesHighlighted={note.linesHighlighted}
onChange={this.handleUpdateContent} onChange={this.handleUpdateContent}
isLocked={this.state.isLocked}
ignorePreviewPointerEvents={ignorePreviewPointerEvents} ignorePreviewPointerEvents={ignorePreviewPointerEvents}
getNote={this.getNote}
RTL={config.editor.rtlEnabled && this.state.RTL} RTL={config.editor.rtlEnabled && this.state.RTL}
/> />
) )
@@ -473,6 +478,7 @@ class MarkdownNoteDetail extends React.Component {
linesHighlighted={note.linesHighlighted} linesHighlighted={note.linesHighlighted}
onChange={this.handleUpdateContent} onChange={this.handleUpdateContent}
ignorePreviewPointerEvents={ignorePreviewPointerEvents} ignorePreviewPointerEvents={ignorePreviewPointerEvents}
getNote={this.getNote}
RTL={config.editor.rtlEnabled && this.state.RTL} RTL={config.editor.rtlEnabled && this.state.RTL}
/> />
) )

View File

@@ -859,8 +859,15 @@ class SnippetNoteDetail extends React.Component {
indentSize={editorIndentSize} indentSize={editorIndentSize}
displayLineNumbers={config.editor.displayLineNumbers} displayLineNumbers={config.editor.displayLineNumbers}
matchingPairs={config.editor.matchingPairs} matchingPairs={config.editor.matchingPairs}
matchingCloseBefore={config.editor.matchingCloseBefore}
matchingTriples={config.editor.matchingTriples} matchingTriples={config.editor.matchingTriples}
explodingPairs={config.editor.explodingPairs} explodingPairs={config.editor.explodingPairs}
codeBlockMatchingPairs={config.editor.codeBlockMatchingPairs}
codeBlockMatchingCloseBefore={
config.editor.codeBlockMatchingCloseBefore
}
codeBlockMatchingTriples={config.editor.codeBlockMatchingTriples}
codeBlockExplodingPairs={config.editor.codeBlockExplodingPairs}
keyMap={config.editor.keyMap} keyMap={config.editor.keyMap}
scrollPastEnd={config.editor.scrollPastEnd} scrollPastEnd={config.editor.scrollPastEnd}
fetchUrlTitle={config.editor.fetchUrlTitle} fetchUrlTitle={config.editor.fetchUrlTitle}
@@ -870,6 +877,9 @@ class SnippetNoteDetail extends React.Component {
enableSmartPaste={config.editor.enableSmartPaste} enableSmartPaste={config.editor.enableSmartPaste}
hotkey={config.hotkey} hotkey={config.hotkey}
autoDetect={autoDetect} autoDetect={autoDetect}
dateFormatISO8601={config.editor.dateFormatISO8601}
storageKey={storageKey}
noteKey={note.key}
/> />
)} )}
</div> </div>

View File

@@ -18,6 +18,7 @@ import { getLocales } from 'browser/lib/Languages'
import applyShortcuts from 'browser/main/lib/shortcutManager' import applyShortcuts from 'browser/main/lib/shortcutManager'
import { chooseTheme, applyTheme } from 'browser/main/lib/ThemeManager' import { chooseTheme, applyTheme } from 'browser/main/lib/ThemeManager'
import { push } from 'connected-react-router' import { push } from 'connected-react-router'
import { ipcRenderer } from 'electron'
const path = require('path') const path = require('path')
const electron = require('electron') const electron = require('electron')
@@ -184,6 +185,7 @@ class Main extends React.Component {
this.toggleMenuBarVisible.bind(this) this.toggleMenuBarVisible.bind(this)
) )
eventEmitter.on('dispatch:push', this.changeRoutePush.bind(this)) eventEmitter.on('dispatch:push', this.changeRoutePush.bind(this))
eventEmitter.on('update', () => ipcRenderer.send('update-check', 'manual'))
} }
componentWillUnmount() { componentWillUnmount() {

View File

@@ -21,6 +21,7 @@ import Markdown from '../../lib/markdown'
import i18n from 'browser/lib/i18n' import i18n from 'browser/lib/i18n'
import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote' import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
import context from 'browser/lib/context' import context from 'browser/lib/context'
import filenamify from 'filenamify'
import queryString from 'query-string' import queryString from 'query-string'
const { remote } = require('electron') const { remote } = require('electron')
@@ -634,6 +635,38 @@ class NoteList extends React.Component {
this.selectNextNote() this.selectNextNote()
} }
handleExportClick(e, note, fileType) {
const options = {
defaultPath: filenamify(note.title, {
replacement: '_'
}),
filters: [{ name: 'Documents', extensions: [fileType] }],
properties: ['openFile', 'createDirectory']
}
dialog.showSaveDialog(remote.getCurrentWindow(), options, filename => {
if (filename) {
const { config } = this.props
dataApi
.exportNoteAs(note, filename, fileType, config)
.then(res => {
dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'info',
message: `Exported to ${filename}`
})
})
.catch(err => {
dialog.showErrorBox(
'Export error',
err ? err.message || err : 'Unexpected error during export'
)
throw err
})
}
})
}
handleNoteContextMenu(e, uniqueKey) { handleNoteContextMenu(e, uniqueKey) {
const { location } = this.props const { location } = this.props
const { selectedNoteKeys } = this.state const { selectedNoteKeys } = this.state
@@ -689,9 +722,40 @@ class NoteList extends React.Component {
click: this.copyNoteLink.bind(this, note) click: this.copyNoteLink.bind(this, note)
} }
) )
if (note.type === 'MARKDOWN_NOTE') { if (note.type === 'MARKDOWN_NOTE') {
templates.push(
{
type: 'separator'
},
{
label: i18n.__('Export Note'),
submenu: [
{
label: i18n.__('Export as Plain Text (.txt)'),
click: e => this.handleExportClick(e, note, 'txt')
},
{
label: i18n.__('Export as Markdown (.md)'),
click: e => this.handleExportClick(e, note, 'md')
},
{
label: i18n.__('Export as HTML (.html)'),
click: e => this.handleExportClick(e, note, 'html')
},
{
label: i18n.__('Export as PDF (.pdf)'),
click: e => this.handleExportClick(e, note, 'pdf')
}
]
}
)
if (note.blog && note.blog.blogLink && note.blog.blogId) { if (note.blog && note.blog.blogLink && note.blog.blogId) {
templates.push( templates.push(
{
type: 'separator'
},
{ {
label: updateLabel, label: updateLabel,
click: this.publishMarkdown.bind(this) click: this.publishMarkdown.bind(this)
@@ -702,10 +766,15 @@ class NoteList extends React.Component {
} }
) )
} else { } else {
templates.push({ templates.push(
label: publishLabel, {
click: this.publishMarkdown.bind(this) type: 'separator'
}) },
{
label: publishLabel,
click: this.publishMarkdown.bind(this)
}
)
} }
} }
} }

View File

@@ -43,12 +43,20 @@ class StorageItem extends React.Component {
label: i18n.__('Export Storage'), label: i18n.__('Export Storage'),
submenu: [ submenu: [
{ {
label: i18n.__('Export as txt'), label: i18n.__('Export as Plain Text (.txt)'),
click: e => this.handleExportStorageClick(e, 'txt') click: e => this.handleExportStorageClick(e, 'txt')
}, },
{ {
label: i18n.__('Export as md'), label: i18n.__('Export as Markdown (.md)'),
click: e => this.handleExportStorageClick(e, 'md') click: e => this.handleExportStorageClick(e, 'md')
},
{
label: i18n.__('Export as HTML (.html)'),
click: e => this.handleExportStorageClick(e, 'html')
},
{
label: i18n.__('Export as PDF (.pdf)'),
click: e => this.handleExportStorageClick(e, 'pdf')
} }
] ]
}, },
@@ -97,14 +105,28 @@ class StorageItem extends React.Component {
} }
dialog.showOpenDialog(remote.getCurrentWindow(), options, paths => { dialog.showOpenDialog(remote.getCurrentWindow(), options, paths => {
if (paths && paths.length === 1) { if (paths && paths.length === 1) {
const { storage, dispatch } = this.props const { storage, dispatch, config } = this.props
dataApi.exportStorage(storage.key, fileType, paths[0]).then(data => { dataApi
dispatch({ .exportStorage(storage.key, fileType, paths[0], config)
type: 'EXPORT_STORAGE', .then(data => {
storage: data.storage, dialog.showMessageBox(remote.getCurrentWindow(), {
fileType: data.fileType type: 'info',
message: `Exported to ${paths[0]}`
})
dispatch({
type: 'EXPORT_STORAGE',
storage: data.storage,
fileType: data.fileType
})
})
.catch(error => {
dialog.showErrorBox(
'Export error',
error ? error.message || error : 'Unexpected error during export'
)
throw error
}) })
})
} }
}) })
} }
@@ -166,12 +188,20 @@ class StorageItem extends React.Component {
label: i18n.__('Export Folder'), label: i18n.__('Export Folder'),
submenu: [ submenu: [
{ {
label: i18n.__('Export as txt'), label: i18n.__('Export as Plain Text (.txt)'),
click: e => this.handleExportFolderClick(e, folder, 'txt') click: e => this.handleExportFolderClick(e, folder, 'txt')
}, },
{ {
label: i18n.__('Export as md'), label: i18n.__('Export as Markdown (.md)'),
click: e => this.handleExportFolderClick(e, folder, 'md') click: e => this.handleExportFolderClick(e, folder, 'md')
},
{
label: i18n.__('Export as HTML (.html)'),
click: e => this.handleExportFolderClick(e, folder, 'html')
},
{
label: i18n.__('Export as PDF (.pdf)'),
click: e => this.handleExportFolderClick(e, folder, 'pdf')
} }
] ]
}, },
@@ -202,30 +232,28 @@ class StorageItem extends React.Component {
} }
dialog.showOpenDialog(remote.getCurrentWindow(), options, paths => { dialog.showOpenDialog(remote.getCurrentWindow(), options, paths => {
if (paths && paths.length === 1) { if (paths && paths.length === 1) {
const { storage, dispatch } = this.props const { storage, dispatch, config } = this.props
dataApi dataApi
.exportFolder(storage.key, folder.key, fileType, paths[0]) .exportFolder(storage.key, folder.key, fileType, paths[0], config)
.then(data => { .then(data => {
dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'info',
message: `Exported to ${paths[0]}`
})
dispatch({ dispatch({
type: 'EXPORT_FOLDER', type: 'EXPORT_FOLDER',
storage: data.storage, storage: data.storage,
folderKey: data.folderKey, folderKey: data.folderKey,
fileType: data.fileType fileType: data.fileType
}) })
return data
}) })
.then(data => { .catch(error => {
dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'info',
message: 'Exported to "' + data.exportDir + '"'
})
})
.catch(err => {
dialog.showErrorBox( dialog.showErrorBox(
'Export error', 'Export error',
err ? err.message || err : 'Unexpected error during export' error ? error.message || error : 'Unexpected error during export'
) )
throw err throw error
}) })
} }
}) })

View File

@@ -26,6 +26,8 @@ import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
import ColorPicker from 'browser/components/ColorPicker' import ColorPicker from 'browser/components/ColorPicker'
import { every, sortBy } from 'lodash' import { every, sortBy } from 'lodash'
const { dialog } = remote
function matchActiveTags(tags, activeTags) { function matchActiveTags(tags, activeTags) {
return every(activeTags, v => tags.indexOf(v) >= 0) return every(activeTags, v => tags.indexOf(v) >= 0)
} }
@@ -63,15 +65,12 @@ class SideNav extends React.Component {
} }
deleteTag(tag) { deleteTag(tag) {
const selectedButton = remote.dialog.showMessageBox( const selectedButton = dialog.showMessageBox(remote.getCurrentWindow(), {
remote.getCurrentWindow(), type: 'warning',
{ message: i18n.__('Confirm tag deletion'),
type: 'warning', detail: i18n.__('This will permanently remove this tag.'),
message: i18n.__('Confirm tag deletion'), buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
detail: i18n.__('This will permanently remove this tag.'), })
buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
}
)
if (selectedButton === 0) { if (selectedButton === 0) {
const { const {
@@ -155,28 +154,80 @@ class SideNav extends React.Component {
} }
handleTagContextMenu(e, tag) { handleTagContextMenu(e, tag) {
const menu = [] context.popup([
{
label: i18n.__('Rename Tag'),
click: this.handleRenameTagClick.bind(this, tag)
},
{
label: i18n.__('Customize Color'),
click: this.displayColorPicker.bind(
this,
tag,
e.target.getBoundingClientRect()
)
},
{
type: 'separator'
},
{
label: i18n.__('Export Tag'),
submenu: [
{
label: i18n.__('Export as Plain Text (.txt)'),
click: e => this.handleExportTagClick(e, tag, 'txt')
},
{
label: i18n.__('Export as Markdown (.md)'),
click: e => this.handleExportTagClick(e, tag, 'md')
},
{
label: i18n.__('Export as HTML (.html)'),
click: e => this.handleExportTagClick(e, tag, 'html')
},
{
label: i18n.__('Export as PDF (.pdf)'),
click: e => this.handleExportTagClick(e, tag, 'pdf')
}
]
},
{
type: 'separator'
},
{
label: i18n.__('Delete Tag'),
click: this.deleteTag.bind(this, tag)
}
])
}
menu.push({ handleExportTagClick(e, tag, fileType) {
label: i18n.__('Delete Tag'), const options = {
click: this.deleteTag.bind(this, tag) properties: ['openDirectory', 'createDirectory'],
buttonLabel: i18n.__('Select directory'),
title: i18n.__('Select a folder to export the files to'),
multiSelections: false
}
dialog.showOpenDialog(remote.getCurrentWindow(), options, paths => {
if (paths && paths.length === 1) {
const { data, config } = this.props
dataApi
.exportTag(data, tag, fileType, paths[0], config)
.then(data => {
dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'info',
message: `Exported to ${paths[0]}`
})
})
.catch(error => {
dialog.showErrorBox(
'Export error',
error ? error.message || error : 'Unexpected error during export'
)
throw error
})
}
}) })
menu.push({
label: i18n.__('Customize Color'),
click: this.displayColorPicker.bind(
this,
tag,
e.target.getBoundingClientRect()
)
})
menu.push({
label: i18n.__('Rename Tag'),
click: this.handleRenameTagClick.bind(this, tag)
})
context.popup(menu)
} }
dismissColorPicker() { dismissColorPicker() {
@@ -330,6 +381,7 @@ class SideNav extends React.Component {
dispatch={dispatch} dispatch={dispatch}
onSortEnd={this.onSortEnd.bind(this)(storage)} onSortEnd={this.onSortEnd.bind(this)(storage)}
useDragHandle useDragHandle
config={config}
/> />
) )
}) })

View File

@@ -12,6 +12,7 @@ import DevTools from './DevTools'
require('./lib/ipcClient') require('./lib/ipcClient')
require('../lib/customMeta') require('../lib/customMeta')
import i18n from 'browser/lib/i18n' import i18n from 'browser/lib/i18n'
import ConfigManager from './lib/ConfigManager'
const electron = require('electron') 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( ReactDOM.render(
<Provider store={store}> <Provider store={store}>
<ConnectedRouter history={history}> <ConnectedRouter history={history}>
@@ -147,8 +164,12 @@ ReactDOM.render(
}) })
ipcRenderer.on('update-found', function() { ipcRenderer.on('update-found', function() {
notify('Update found!', { downloadUpdate()
body: 'Preparing to update...' })
ipcRenderer.on('update-not-found', function(_, msg) {
notify('Update not found!', {
body: msg
}) })
}) })

View File

@@ -18,9 +18,9 @@ const DEFAULT_MARKDOWN_LINT_CONFIG = `{
const DEFAULT_CSS_CONFIG = ` const DEFAULT_CSS_CONFIG = `
/* Drop Your Custom CSS Code Here */ /* Drop Your Custom CSS Code Here */
[data-theme="default"] p code, [data-theme="default"] p code.inline,
[data-theme="default"] li code, [data-theme="default"] li code.inline,
[data-theme="default"] td code [data-theme="default"] td code.inline
{ {
padding: 2px; padding: 2px;
border-width: 1px; border-width: 1px;
@@ -86,8 +86,13 @@ export const DEFAULT_CONFIG = {
rulers: [80, 120], rulers: [80, 120],
displayLineNumbers: true, displayLineNumbers: true,
matchingPairs: '()[]{}\'\'""$$**``~~__', matchingPairs: '()[]{}\'\'""$$**``~~__',
matchingCloseBefore: ')]}\'":;>',
matchingTriples: '```"""\'\'\'', matchingTriples: '```"""\'\'\'',
explodingPairs: '[]{}``$$', explodingPairs: '[]{}``$$',
codeBlockMatchingPairs: '()[]{}\'\'""``',
codeBlockMatchingCloseBefore: ')]}\'":;>',
codeBlockMatchingTriples: '',
codeBlockExplodingPairs: '[]{}``',
switchPreview: 'BLUR', // 'BLUR', 'DBL_CLICK', 'RIGHTCLICK' switchPreview: 'BLUR', // 'BLUR', 'DBL_CLICK', 'RIGHTCLICK'
delfaultStatus: 'PREVIEW', // 'PREVIEW', 'CODE' delfaultStatus: 'PREVIEW', // 'PREVIEW', 'CODE'
scrollPastEnd: false, scrollPastEnd: false,
@@ -100,6 +105,7 @@ export const DEFAULT_CONFIG = {
enableSmartPaste: false, enableSmartPaste: false,
enableMarkdownLint: false, enableMarkdownLint: false,
customMarkdownLintConfig: DEFAULT_MARKDOWN_LINT_CONFIG, customMarkdownLintConfig: DEFAULT_MARKDOWN_LINT_CONFIG,
dateFormatISO8601: false,
prettierConfig: `{ prettierConfig: `{
"trailingComma": "es5", "trailingComma": "es5",
"tabWidth": 2, "tabWidth": 2,
@@ -138,7 +144,15 @@ export const DEFAULT_CONFIG = {
username: '', username: '',
password: '' password: ''
}, },
coloredTags: {} export: {
metadata: 'DONT_EXPORT', // 'DONT_EXPORT', 'MERGE_HEADER', 'MERGE_VARIABLE'
variable: 'boostnote',
prefixAttachmentFolder: false
},
coloredTags: {},
wakatime: {
key: null
}
} }
function validate(config) { function validate(config) {
@@ -254,6 +268,12 @@ function assignConfigValues(originalConfig, rcConfig) {
originalConfig.hotkey, originalConfig.hotkey,
rcConfig.hotkey rcConfig.hotkey
) )
config.wakatime = Object.assign(
{},
DEFAULT_CONFIG.wakatime,
originalConfig.wakatime,
rcConfig.wakatime
)
config.blog = Object.assign( config.blog = Object.assign(
{}, {},
DEFAULT_CONFIG.blog, DEFAULT_CONFIG.blog,

View File

@@ -1,4 +1,5 @@
import ConfigManager from 'browser/main/lib/ConfigManager' import ConfigManager from 'browser/main/lib/ConfigManager'
import uiThemes from 'browser/lib/ui-themes'
const saveChanges = newConfig => { const saveChanges = newConfig => {
ConfigManager.set(newConfig) ConfigManager.set(newConfig)
@@ -40,14 +41,7 @@ const chooseTheme = config => {
} }
const applyTheme = theme => { const applyTheme = theme => {
const supportedThemes = [ if (uiThemes.some(item => item.name === theme)) {
'dark',
'white',
'solarized-dark',
'monokai',
'dracula'
]
if (supportedThemes.indexOf(theme) !== -1) {
document.body.setAttribute('data-theme', theme) document.body.setAttribute('data-theme', theme)
if (document.body.querySelector('.MarkdownPreview')) { if (document.body.querySelector('.MarkdownPreview')) {
document.body document.body

View File

@@ -706,14 +706,15 @@ function replaceNoteKeyWithNewNoteKey(noteContent, oldNoteKey, newNoteKey) {
} }
/** /**
* @description Deletes all :storage and noteKey references from the given input. * @description replace all :storage references with given destination folder.
* @param input Input in which the references should be deleted * @param input Input in which the references should be replaced
* @param noteKey Key of the current note * @param noteKey Key of the current note
* @param destinationFolder Destination folder of the attachements
* @returns {String} Input without the references * @returns {String} Input without the references
*/ */
function removeStorageAndNoteReferences(input, noteKey) { function replaceStorageReferences(input, noteKey, destinationFolder) {
return input.replace( return input.replace(
new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER + '.*?("|\\))', 'g'), new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER + '[^"\\)<\\s]+', 'g'),
function(match) { function(match) {
return match return match
.replace(new RegExp(mdurl.encode(path.win32.sep), 'g'), path.posix.sep) .replace(new RegExp(mdurl.encode(path.win32.sep), 'g'), path.posix.sep)
@@ -735,7 +736,7 @@ function removeStorageAndNoteReferences(input, noteKey) {
')?', ')?',
'g' 'g'
), ),
DESTINATION_FOLDER destinationFolder
) )
} }
) )
@@ -1101,8 +1102,8 @@ module.exports = {
getAttachmentsInMarkdownContent, getAttachmentsInMarkdownContent,
getAbsolutePathsOfAttachmentsInContent, getAbsolutePathsOfAttachmentsInContent,
importAttachments, importAttachments,
removeStorageAndNoteReferences,
removeAttachmentsByPaths, removeAttachmentsByPaths,
replaceStorageReferences,
deleteAttachmentFolder, deleteAttachmentFolder,
deleteAttachmentsNotPresentInNote, deleteAttachmentsNotPresentInNote,
getAttachmentsPathAndStatus, getAttachmentsPathAndStatus,

View File

@@ -1,5 +1,6 @@
const fs = require('fs') import fs from 'fs'
const path = require('path') import fx from 'fs-extra'
import path from 'path'
/** /**
* @description Copy a file from source to destination * @description Copy a file from source to destination
@@ -14,7 +15,8 @@ function copyFile(srcPath, dstPath) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const dstFolder = path.dirname(dstPath) const dstFolder = path.dirname(dstPath)
if (!fs.existsSync(dstFolder)) fs.mkdirSync(dstFolder)
fx.ensureDirSync(dstFolder)
const input = fs.createReadStream(decodeURI(srcPath)) const input = fs.createReadStream(decodeURI(srcPath))
const output = fs.createWriteStream(dstPath) const output = fs.createWriteStream(dstPath)

View File

@@ -1,15 +1,16 @@
import { findStorage } from 'browser/lib/findStorage' import { findStorage } from 'browser/lib/findStorage'
import resolveStorageData from './resolveStorageData' import resolveStorageData from './resolveStorageData'
import resolveStorageNotes from './resolveStorageNotes' import resolveStorageNotes from './resolveStorageNotes'
import getFilename from './getFilename'
import exportNote from './exportNote' import exportNote from './exportNote'
import filenamify from 'filenamify' import getContentFormatter from './getContentFormatter'
import * as path from 'path'
/** /**
* @param {String} storageKey * @param {String} storageKey
* @param {String} folderKey * @param {String} folderKey
* @param {String} fileType * @param {String} fileType
* @param {String} exportDir * @param {String} exportDir
* @param {Object} config
* *
* @return {Object} * @return {Object}
* ``` * ```
@@ -22,7 +23,7 @@ import * as path from 'path'
* ``` * ```
*/ */
function exportFolder(storageKey, folderKey, fileType, exportDir) { function exportFolder(storageKey, folderKey, fileType, exportDir, config) {
let targetStorage let targetStorage
try { try {
targetStorage = findStorage(storageKey) targetStorage = findStorage(storageKey)
@@ -30,39 +31,34 @@ function exportFolder(storageKey, folderKey, fileType, exportDir) {
return Promise.reject(e) return Promise.reject(e)
} }
const deduplicator = {}
return resolveStorageData(targetStorage) return resolveStorageData(targetStorage)
.then(function assignNotes(storage) { .then(storage => {
return resolveStorageNotes(storage).then(notes => { return resolveStorageNotes(storage).then(notes => ({
return { storage,
storage, notes: notes.filter(
notes note =>
} note.folder === folderKey &&
}) !note.isTrashed &&
note.type === 'MARKDOWN_NOTE'
)
}))
}) })
.then(function exportNotes(data) { .then(({ storage, notes }) => {
const { storage, notes } = data const contentFormatter = getContentFormatter(storage, fileType, config)
return Promise.all( return Promise.all(
notes notes.map(note => {
.filter( const targetPath = getFilename(
note => note,
note.folder === folderKey && fileType,
note.isTrashed === false && exportDir,
note.type === 'MARKDOWN_NOTE' deduplicator
) )
.map(note => {
const notePath = path.join( return exportNote(storage.key, note, targetPath, contentFormatter)
exportDir, })
`${filenamify(note.title, { replacement: '_' })}.${fileType}`
)
return exportNote(
note.key,
storage.path,
note.content,
notePath,
null
)
})
).then(() => ({ ).then(() => ({
storage, storage,
folderKey, folderKey,

View File

@@ -4,58 +4,35 @@ import { findStorage } from 'browser/lib/findStorage'
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const attachmentManagement = require('./attachmentManagement')
/** /**
* Export note together with attachments * Export note together with attachments
* *
* If attachments are stored in the storage, creates 'attachments' subfolder in target directory * If attachments are stored in the storage, creates 'attachments' subfolder in target directory
* and copies attachments to it. Changes links to images in the content of the note * and copies attachments to it. Changes links to images in the content of the note
* *
* @param {String} nodeKey key of the node that should be exported
* @param {String} storageKey or storage path * @param {String} storageKey or storage path
* @param {String} noteContent Content to export * @param {Object} note Note to export
* @param {String} targetPath Path to exported file * @param {String} targetPath Path to exported file
* @param {function} outputFormatter * @param {function} outputFormatter
* @return {Promise.<*[]>} * @return {Promise.<*[]>}
*/ */
function exportNote( function exportNote(storageKey, note, targetPath, outputFormatter) {
nodeKey,
storageKey,
noteContent,
targetPath,
outputFormatter
) {
const storagePath = path.isAbsolute(storageKey) const storagePath = path.isAbsolute(storageKey)
? storageKey ? storageKey
: findStorage(storageKey).path : findStorage(storageKey).path
const exportTasks = [] const exportTasks = []
if (!storagePath) { if (!storagePath) {
throw new Error('Storage path is not found') throw new Error('Storage path is not found')
} }
const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent(
noteContent,
storagePath
)
attachmentsAbsolutePaths.forEach(attachment => {
exportTasks.push({
src: attachment,
dst: attachmentManagement.DESTINATION_FOLDER
})
})
let exportedData = attachmentManagement.removeStorageAndNoteReferences( const exportedData = Promise.resolve(
noteContent, outputFormatter
nodeKey ? outputFormatter(note, targetPath, exportTasks)
: note.content
) )
if (outputFormatter) {
exportedData = outputFormatter(exportedData, exportTasks, targetPath)
} else {
exportedData = Promise.resolve(exportedData)
}
const tasks = prepareTasks(exportTasks, storagePath, path.dirname(targetPath)) const tasks = prepareTasks(exportTasks, storagePath, path.dirname(targetPath))
return Promise.all(tasks.map(task => copyFile(task.src, task.dst))) return Promise.all(tasks.map(task => copyFile(task.src, task.dst)))
@@ -63,9 +40,9 @@ function exportNote(
.then(data => { .then(data => {
return saveToFile(data, targetPath) return saveToFile(data, targetPath)
}) })
.catch(err => { .catch(error => {
rollbackExport(tasks) rollbackExport(tasks)
throw err throw error
}) })
} }
@@ -107,14 +84,14 @@ function rollbackExport(tasks) {
} }
if (fs.existsSync(fullpath)) { if (fs.existsSync(fullpath)) {
fs.unlink(fullpath) fs.unlinkSync(fullpath)
folders.add(path.dirname(fullpath)) folders.add(path.dirname(fullpath))
} }
}) })
folders.forEach(folder => { folders.forEach(folder => {
if (fs.readdirSync(folder).length === 0) { if (fs.readdirSync(folder).length === 0) {
fs.rmdir(folder) fs.rmdirSync(folder)
} }
}) })
} }

View 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

View File

@@ -2,13 +2,17 @@ import { findStorage } from 'browser/lib/findStorage'
import resolveStorageData from './resolveStorageData' import resolveStorageData from './resolveStorageData'
import resolveStorageNotes from './resolveStorageNotes' import resolveStorageNotes from './resolveStorageNotes'
import filenamify from 'filenamify' import filenamify from 'filenamify'
import * as path from 'path' import path from 'path'
import * as fs from 'fs' import fs from 'fs'
import exportNote from './exportNote'
import getContentFormatter from './getContentFormatter'
import getFilename from './getFilename'
/** /**
* @param {String} storageKey * @param {String} storageKey
* @param {String} fileType * @param {String} fileType
* @param {String} exportDir * @param {String} exportDir
* @param {Object} config
* *
* @return {Object} * @return {Object}
* ``` * ```
@@ -20,7 +24,7 @@ import * as fs from 'fs'
* ``` * ```
*/ */
function exportStorage(storageKey, fileType, exportDir) { function exportStorage(storageKey, fileType, exportDir, config) {
let targetStorage let targetStorage
try { try {
targetStorage = findStorage(storageKey) targetStorage = findStorage(storageKey)
@@ -29,39 +33,52 @@ function exportStorage(storageKey, fileType, exportDir) {
} }
return resolveStorageData(targetStorage) return resolveStorageData(targetStorage)
.then(storage => .then(storage => {
resolveStorageNotes(storage).then(notes => ({ storage, notes })) return resolveStorageNotes(storage).then(notes => ({
) storage,
.then(function exportNotes(data) { notes: notes.filter(
const { storage, notes } = data note => !note.isTrashed && note.type === 'MARKDOWN_NOTE'
)
}))
})
.then(({ storage, notes }) => {
const contentFormatter = getContentFormatter(storage, fileType, config)
const folderNamesMapping = {} const folderNamesMapping = {}
const deduplicators = {}
storage.folders.forEach(folder => { storage.folders.forEach(folder => {
const folderExportedDir = path.join( const folderExportedDir = path.join(
exportDir, exportDir,
filenamify(folder.name, { replacement: '_' }) filenamify(folder.name, { replacement: '_' })
) )
folderNamesMapping[folder.key] = folderExportedDir folderNamesMapping[folder.key] = folderExportedDir
// make sure directory exists // make sure directory exists
try { try {
fs.mkdirSync(folderExportedDir) fs.mkdirSync(folderExportedDir)
} catch (e) {} } catch (e) {}
})
notes
.filter(note => !note.isTrashed && note.type === 'MARKDOWN_NOTE')
.forEach(markdownNote => {
const folderExportedDir = folderNamesMapping[markdownNote.folder]
const snippetName = `${filenamify(markdownNote.title, {
replacement: '_'
})}.${fileType}`
const notePath = path.join(folderExportedDir, snippetName)
fs.writeFileSync(notePath, markdownNote.content)
})
return { deduplicators[folder.key] = {}
})
return Promise.all(
notes.map(note => {
const targetPath = getFilename(
note,
fileType,
folderNamesMapping[note.folder],
deduplicators[note.folder]
)
return exportNote(storage.key, note, targetPath, contentFormatter)
})
).then(() => ({
storage, storage,
fileType, fileType,
exportDir exportDir
} }))
}) })
} }

View 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

View 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
}

View 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}`
}
}

View 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()
})
})
})
}
}

View 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
}

View 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

View File

@@ -15,11 +15,14 @@ const dataApi = {
updateNote: require('./updateNote'), updateNote: require('./updateNote'),
deleteNote: require('./deleteNote'), deleteNote: require('./deleteNote'),
moveNote: require('./moveNote'), moveNote: require('./moveNote'),
exportNoteAs: require('./exportNoteAs'),
migrateFromV5Storage: require('./migrateFromV5Storage'), migrateFromV5Storage: require('./migrateFromV5Storage'),
createSnippet: require('./createSnippet'), createSnippet: require('./createSnippet'),
deleteSnippet: require('./deleteSnippet'), deleteSnippet: require('./deleteSnippet'),
updateSnippet: require('./updateSnippet'), updateSnippet: require('./updateSnippet'),
fetchSnippet: require('./fetchSnippet'), fetchSnippet: require('./fetchSnippet'),
exportTag: require('./exportTag'),
getFilename: require('./getFilename'),
_migrateFromV6Storage: require('./migrateFromV6Storage'), _migrateFromV6Storage: require('./migrateFromV6Storage'),
_resolveStorageData: require('./resolveStorageData'), _resolveStorageData: require('./resolveStorageData'),

View File

@@ -139,6 +139,13 @@ div[id^="firstRow"]
margin-right 10px margin-right 10px
font-size 14px 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 .group-section-control
flex 1 flex 1
margin-left 5px margin-left 5px

View 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'
/>
&nbsp;
{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)

View File

@@ -16,25 +16,78 @@ class InfoTab extends React.Component {
super(props) super(props)
this.state = { 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) { handleLinkClick(e) {
shell.openExternal(e.currentTarget.href) shell.openExternal(e.currentTarget.href)
e.preventDefault() e.preventDefault()
} }
handleConfigChange(e) { 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 }) 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) { handleSaveButtonClick(e) {
const newConfig = { const newConfig = this.state.config
amaEnabled: this.state.config.amaEnabled
}
if (!newConfig.amaEnabled) { if (!newConfig.amaEnabled) {
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('DISABLE_AMA') 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() { infoMessage() {
const { amaMessage } = this.state const { amaMessage } = this.state
return amaMessage ? <p styleName='policy-confirm'>{amaMessage}</p> : null return amaMessage ? <p styleName='policy-confirm'>{amaMessage}</p> : null
} }
handleAutoUpdateChange() {
const { autoUpdateEnabled } = this.handleConfigChange()
ConfigManager.set({ autoUpdateEnabled })
}
render() { render() {
return ( return (
<div styleName='root'> <div styleName='root'>
@@ -134,6 +184,40 @@ class InfoTab extends React.Component {
<hr /> <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='group-header--sub'>{i18n.__('About')}</div>
<div styleName='top'> <div styleName='top'>
@@ -181,7 +265,8 @@ class InfoTab extends React.Component {
<label> <label>
<input <input
type='checkbox' type='checkbox'
onChange={this.toggleAutoUpdate.bind(this)} ref='autoUpdateEnabled'
onChange={() => this.handleAutoUpdateChange()}
checked={this.state.config.autoUpdateEnabled} checked={this.state.config.autoUpdateEnabled}
/> />
{i18n.__('Enable Auto Update')} {i18n.__('Enable Auto Update')}

View File

@@ -33,6 +33,35 @@
.separate-line .separate-line
margin 40px 0 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 .policy-submit
margin-top 10px margin-top 10px
height 35px height 35px

View 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'
/>
&nbsp;
{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)

View File

@@ -35,10 +35,18 @@ class SnippetEditor extends React.Component {
foldGutter: true, foldGutter: true,
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'], gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
autoCloseBrackets: { autoCloseBrackets: {
pairs: this.props.matchingPairs, codeBlock: {
triples: this.props.matchingTriples, pairs: this.props.codeBlockMatchingPairs,
explode: this.props.explodingPairs, closeBefore: this.props.codeBlockMatchingCloseBefore,
override: true 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' mode: 'null'
}) })

View File

@@ -152,8 +152,15 @@ class SnippetTab extends React.Component {
rulers={config.editor.rulers} rulers={config.editor.rulers}
displayLineNumbers={config.editor.displayLineNumbers} displayLineNumbers={config.editor.displayLineNumbers}
matchingPairs={config.editor.matchingPairs} matchingPairs={config.editor.matchingPairs}
matchingCloseBefore={config.editor.matchingCloseBefore}
matchingTriples={config.editor.matchingTriples} matchingTriples={config.editor.matchingTriples}
explodingPairs={config.editor.explodingPairs} explodingPairs={config.editor.explodingPairs}
codeBlockMatchingPairs={config.editor.codeBlockMatchingPairs}
codeBlockMatchingCloseBefore={
config.editor.codeBlockMatchingCloseBefore
}
codeBlockMatchingTriples={config.editor.codeBlockMatchingTriples}
codeBlockExplodingPairs={config.editor.codeBlockExplodingPairs}
scrollPastEnd={config.editor.scrollPastEnd} scrollPastEnd={config.editor.scrollPastEnd}
onRef={ref => { onRef={ref => {
this.snippetEditor = ref this.snippetEditor = ref

View File

@@ -124,14 +124,21 @@ class UiTab extends React.Component {
enableFrontMatterTitle: this.refs.enableFrontMatterTitle.checked, enableFrontMatterTitle: this.refs.enableFrontMatterTitle.checked,
frontMatterTitleField: this.refs.frontMatterTitleField.value, frontMatterTitleField: this.refs.frontMatterTitleField.value,
matchingPairs: this.refs.matchingPairs.value, matchingPairs: this.refs.matchingPairs.value,
matchingCloseBefore: this.refs.matchingCloseBefore.value,
matchingTriples: this.refs.matchingTriples.value, matchingTriples: this.refs.matchingTriples.value,
explodingPairs: this.refs.explodingPairs.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, spellcheck: this.refs.spellcheck.checked,
enableSmartPaste: this.refs.enableSmartPaste.checked, enableSmartPaste: this.refs.enableSmartPaste.checked,
enableMarkdownLint: this.refs.enableMarkdownLint.checked, enableMarkdownLint: this.refs.enableMarkdownLint.checked,
customMarkdownLintConfig: this.customMarkdownLintConfigCM customMarkdownLintConfig: this.customMarkdownLintConfigCM
.getCodeMirror() .getCodeMirror()
.getValue(), .getValue(),
dateFormatISO8601: this.refs.dateFormatISO8601.checked,
prettierConfig: this.prettierConfigCM.getCodeMirror().getValue(), prettierConfig: this.prettierConfigCM.getCodeMirror().getValue(),
deleteUnusedAttachments: this.refs.deleteUnusedAttachments.checked, deleteUnusedAttachments: this.refs.deleteUnusedAttachments.checked,
rtlEnabled: this.refs.rtlEnabled.checked rtlEnabled: this.refs.rtlEnabled.checked
@@ -745,6 +752,126 @@ class UiTab extends React.Component {
</div> </div>
</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'> <div styleName='group-checkBoxSection'>
<label> <label>
<input <input
@@ -875,50 +1002,19 @@ class UiTab extends React.Component {
</label> </label>
</div> </div>
<div styleName='group-section'> <div styleName='group-checkBoxSection'>
<div styleName='group-section-label'> <label>
{i18n.__('Matching character pairs')}
</div>
<div styleName='group-section-control'>
<input <input
styleName='group-section-control-input'
value={this.state.config.editor.matchingPairs}
ref='matchingPairs'
onChange={e => this.handleUIChange(e)} onChange={e => this.handleUIChange(e)}
type='text' checked={this.state.config.editor.dateFormatISO8601}
ref='dateFormatISO8601'
type='checkbox'
/> />
</div> &nbsp;
{i18n.__('Date shortcut use iso 8601 format')}
</label>
</div> </div>
<div styleName='group-section'>
<div styleName='group-section-label'>
{i18n.__('Matching character triples')}
</div>
<div styleName='group-section-control'>
<input
styleName='group-section-control-input'
value={this.state.config.editor.matchingTriples}
ref='matchingTriples'
onChange={e => this.handleUIChange(e)}
type='text'
/>
</div>
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>
{i18n.__('Exploding character pairs')}
</div>
<div styleName='group-section-control'>
<input
styleName='group-section-control-input'
value={this.state.config.editor.explodingPairs}
ref='explodingPairs'
onChange={e => this.handleUIChange(e)}
type='text'
/>
</div>
</div>
<div styleName='group-section'> <div styleName='group-section'>
<div styleName='group-section-label'> <div styleName='group-section-label'>
{i18n.__('Custom MarkdownLint Rules')} {i18n.__('Custom MarkdownLint Rules')}

View File

@@ -6,7 +6,9 @@ import UiTab from './UiTab'
import InfoTab from './InfoTab' import InfoTab from './InfoTab'
import Crowdfunding from './Crowdfunding' import Crowdfunding from './Crowdfunding'
import StoragesTab from './StoragesTab' import StoragesTab from './StoragesTab'
import ExportTab from './ExportTab'
import SnippetTab from './SnippetTab' import SnippetTab from './SnippetTab'
import PluginsTab from './PluginsTab'
import Blog from './Blog' import Blog from './Blog'
import ModalEscButton from 'browser/components/ModalEscButton' import ModalEscButton from 'browser/components/ModalEscButton'
import CSSModules from 'browser/lib/CSSModules' import CSSModules from 'browser/lib/CSSModules'
@@ -23,7 +25,8 @@ class Preferences extends React.Component {
currentTab: 'STORAGES', currentTab: 'STORAGES',
UIAlert: '', UIAlert: '',
HotkeyAlert: '', HotkeyAlert: '',
BlogAlert: '' BlogAlert: '',
ExportAlert: ''
} }
} }
@@ -80,8 +83,25 @@ class Preferences extends React.Component {
haveToSave={alert => this.setState({ BlogAlert: alert })} haveToSave={alert => this.setState({ BlogAlert: alert })}
/> />
) )
case 'EXPORT':
return (
<ExportTab
dispatch={dispatch}
config={config}
data={data}
haveToSave={alert => this.setState({ ExportAlert: alert })}
/>
)
case 'SNIPPET': case 'SNIPPET':
return <SnippetTab dispatch={dispatch} config={config} data={data} /> return <SnippetTab dispatch={dispatch} config={config} data={data} />
case 'PLUGINS':
return (
<PluginsTab
dispatch={dispatch}
config={config}
haveToSave={alert => this.setState({ PluginsAlert: alert })}
/>
)
case 'STORAGES': case 'STORAGES':
default: default:
return ( return (
@@ -122,7 +142,13 @@ class Preferences extends React.Component {
{ target: 'INFO', label: i18n.__('About') }, { target: 'INFO', label: i18n.__('About') },
{ target: 'CROWDFUNDING', label: i18n.__('Crowdfunding') }, { target: 'CROWDFUNDING', label: i18n.__('Crowdfunding') },
{ target: 'BLOG', label: i18n.__('Blog'), Blog: this.state.BlogAlert }, { target: 'BLOG', label: i18n.__('Blog'), Blog: this.state.BlogAlert },
{ target: '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 => { const navButtons = tabs.map(tab => {

View 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)))
}
});

View File

@@ -55,202 +55,168 @@
} }
} }
CodeMirror.defineMode( CodeMirror.defineMode('bfm', function (config, baseConfig) {
'bfm', baseConfig.name = 'yaml-frontmatter'
function(config, baseConfig) { const baseMode = CodeMirror.getMode(config, baseConfig)
baseConfig.name = 'yaml-frontmatter'
const baseMode = CodeMirror.getMode(config, baseConfig)
return { return {
startState: function() { startState: function() {
return { return {
baseState: CodeMirror.startState(baseMode), baseState: CodeMirror.startState(baseMode),
basePos: 0, basePos: 0,
baseCur: null, baseCur: null,
overlayPos: 0, overlayPos: 0,
overlayCur: null, overlayCur: null,
streamSeen: null, streamSeen: null,
fencedEndRE: null, fencedEndRE: null,
inTable: false, inTable: false,
rowIndex: 0 rowIndex: 0
} }
}, },
copyState: function(s) { copyState: function(s) {
return { return {
baseState: CodeMirror.copyState(baseMode, s.baseState), baseState: CodeMirror.copyState(baseMode, s.baseState),
basePos: s.basePos, basePos: s.basePos,
baseCur: null, baseCur: null,
overlayPos: s.overlayPos, overlayPos: s.overlayPos,
overlayCur: null, overlayCur: null,
fencedMode: s.fencedMode, fencedMode: s.fencedMode,
fencedState: s.fencedMode fencedState: s.fencedMode ? CodeMirror.copyState(s.fencedMode, s.fencedState) : null,
? CodeMirror.copyState(s.fencedMode, s.fencedState)
: null,
fencedEndRE: s.fencedEndRE, fencedEndRE: s.fencedEndRE,
inTable: s.inTable, inTable: s.inTable,
rowIndex: s.rowIndex rowIndex: s.rowIndex
} }
}, },
token: function(stream, state) { token: function(stream, state) {
const initialPos = stream.pos const initialPos = stream.pos
if (state.fencedEndRE && stream.match(state.fencedEndRE)) { if (state.fencedEndRE) {
if (stream.match(state.fencedEndRE)) {
state.fencedEndRE = null state.fencedEndRE = null
state.fencedMode = null state.fencedMode = null
state.fencedState = null state.fencedState = null
stream.pos = initialPos stream.pos = initialPos
} else if (state.fencedMode) {
return state.fencedMode.token(stream, state.fencedState)
} else { } else {
if (state.fencedMode) {
return state.fencedMode.token(stream, state.fencedState)
}
const match = stream.match(fencedCodeRE, true)
if (match) {
state.fencedEndRE = new RegExp(match[1] + '+ *$')
state.fencedMode = getMode(
match[2],
match[3],
config,
stream.lineOracle.doc.cm
)
if (state.fencedMode) {
state.fencedState = CodeMirror.startState(state.fencedMode)
}
stream.pos = initialPos
}
}
if (
stream != state.streamSeen ||
Math.min(state.basePos, state.overlayPos) < stream.start
) {
state.streamSeen = stream
state.basePos = state.overlayPos = stream.start
}
if (stream.start == state.basePos) {
state.baseCur = baseMode.token(stream, state.baseState)
state.basePos = stream.pos
}
if (stream.start == state.overlayPos) {
stream.pos = stream.start
state.overlayCur = this.overlayToken(stream, state) state.overlayCur = this.overlayToken(stream, state)
state.overlayPos = stream.pos state.overlayPos = stream.pos
}
stream.pos = Math.min(state.basePos, state.overlayPos)
if (state.overlayCur == null) {
return state.baseCur
} else if (state.baseCur != null && state.combineTokens) {
return state.baseCur + ' ' + state.overlayCur
} else {
return state.overlayCur return state.overlayCur
} }
}, }
overlayToken: function(stream, state) { else {
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) const match = stream.match(fencedCodeRE, true)
if (match) { if (match) {
state.fencedEndRE = new RegExp(match[1] + '+ *$') state.fencedEndRE = new RegExp(match[1] + '+ *$')
state.localMode = getMode( state.fencedMode = getMode(match[2], match[3], config, stream.lineOracle.doc.cm)
match[2], if (state.fencedMode) {
match[3], state.fencedState = CodeMirror.startState(state.fencedMode)
config,
stream.lineOracle.doc.cm
)
if (state.localMode) {
state.localState = CodeMirror.startState(state.localMode)
} }
return null stream.pos = initialPos
}
state.combineTokens = true
if (state.inTable) {
if (stream.match(/^\|/)) {
++state.rowIndex
stream.skipToEnd()
if (state.rowIndex === 1) {
return 'table table-separator'
} else if (state.rowIndex % 2 === 0) {
return 'table table-row table-row-even'
} else {
return 'table table-row table-row-odd'
}
} else {
state.inTable = false
stream.skipToEnd()
return null
}
} else if (stream.match(/^\|/)) {
state.inTable = true
state.rowIndex = 0
stream.skipToEnd()
return 'table table-header'
}
stream.skipToEnd()
return null
},
electricChars: baseMode.electricChars,
innerMode: function(state) {
if (state.fencedMode) {
return {
mode: state.fencedMode,
state: state.fencedState
}
} else {
return {
mode: baseMode,
state: state.baseState
}
}
},
blankLine: function(state) {
state.inTable = false
if (state.fencedMode) {
return (
state.fencedMode.blankLine &&
state.fencedMode.blankLine(state.fencedState)
)
} else {
return baseMode.blankLine(state.baseState)
} }
} }
if (stream != state.streamSeen || Math.min(state.basePos, state.overlayPos) < stream.start) {
state.streamSeen = stream
state.basePos = state.overlayPos = stream.start
}
if (stream.start == state.basePos) {
state.baseCur = baseMode.token(stream, state.baseState)
state.basePos = stream.pos
}
if (stream.start == state.overlayPos) {
stream.pos = stream.start
state.overlayCur = this.overlayToken(stream, state)
state.overlayPos = stream.pos
}
stream.pos = Math.min(state.basePos, state.overlayPos)
if (state.overlayCur == null) {
return state.baseCur
}
else if (state.baseCur != null && state.combineTokens) {
return state.baseCur + ' ' + state.overlayCur
}
else {
return state.overlayCur
}
},
overlayToken: function(stream, state) {
state.combineTokens = false
if (state.localMode) {
return state.localMode.token(stream, state.localState) || ''
}
state.combineTokens = true
if (state.inTable) {
if (stream.match(/^\|/)) {
++state.rowIndex
stream.skipToEnd()
if (state.rowIndex === 1) {
return 'table table-separator'
} else if (state.rowIndex % 2 === 0) {
return 'table table-row table-row-even'
} else {
return 'table table-row table-row-odd'
}
} else {
state.inTable = false
stream.skipToEnd()
return null
}
} else if (stream.match(/^\|/)) {
state.inTable = true
state.rowIndex = 0
stream.skipToEnd()
return 'table table-header'
}
stream.skipToEnd()
return null
},
electricChars: baseMode.electricChars,
innerMode: function(state) {
if (state.fencedMode) {
return {
mode: state.fencedMode,
state: state.fencedState
}
} else {
return {
mode: baseMode,
state: state.baseState
}
}
},
blankLine: function(state) {
state.inTable = false
if (state.fencedMode) {
return state.fencedMode.blankLine && state.fencedMode.blankLine(state.fencedState)
} else {
return baseMode.blankLine(state.baseState)
}
} }
}, }
'yaml-frontmatter' }, 'yaml-frontmatter')
)
CodeMirror.defineMIME('text/x-bfm', 'bfm') CodeMirror.defineMIME('text/x-bfm', 'bfm')
@@ -259,4 +225,4 @@
mime: 'text/x-bfm', mime: 'text/x-bfm',
mode: 'bfm' mode: 'bfm'
}) })
}) })

View File

@@ -187,7 +187,7 @@ module.exports = function(grunt) {
} }
ChildProcess.exec( 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) { function(err, stdout, stderr) {
grunt.log.writeln(stdout) grunt.log.writeln(stdout)
if (err) { if (err) {

View File

@@ -26,6 +26,7 @@ if (!singleInstance) {
} }
var isUpdateReady = false var isUpdateReady = false
let updateFound = false
var ghReleasesOpts = { var ghReleasesOpts = {
repo: 'BoostIO/boost-releases', repo: 'BoostIO/boost-releases',
@@ -36,25 +37,33 @@ const updater = new GhReleases(ghReleasesOpts)
// Check for updates // Check for updates
// `status` returns true if there is a new update available // `status` returns true if there is a new update available
function checkUpdate() { function checkUpdate(manualTriggered = false) {
if (!isPackaged) { if (!isPackaged) {
// Prevents app from attempting to update when in dev mode. // Prevents app from attempting to update when in dev mode.
console.log('Updates are disabled in Development mode, see main-app.js') console.log('Updates are disabled in Development mode, see main-app.js')
return true 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 return true
} }
updater.check((err, status) => { updater.check((err, status) => {
if (err) { if (err) {
var isLatest = err.message === 'There is no newer version.' var isLatest = err.message === 'There is no newer version.'
if (!isLatest) console.error('Updater error! %s', err.message) if (!isLatest) console.error('Updater error! %s', err.message)
mainWindow.webContents.send(
'update-not-found',
isLatest ? 'There is no newer version.' : 'Updater error'
)
return return
} }
if (status) { if (status) {
mainWindow.webContents.send('update-found', 'Update available!') mainWindow.webContents.send('update-found', 'Update available!')
updater.download() updateFound = true
} }
}) })
} }
@@ -63,6 +72,7 @@ updater.on('update-downloaded', info => {
if (mainWindow != null) { if (mainWindow != null) {
mainWindow.webContents.send('update-ready', 'Update available!') mainWindow.webContents.send('update-ready', 'Update available!')
isUpdateReady = true 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.on('window-all-closed', function() {
app.quit() app.quit()
}) })
@@ -113,7 +131,7 @@ app.on('ready', function() {
if (isUpdateReady) { if (isUpdateReady) {
mainWindow.webContents.send('update-ready', 'Update available!') mainWindow.webContents.send('update-ready', 'Update available!')
} else { } else {
checkUpdate() checkUpdate(msg === 'manual')
} }
}) })
}, 10 * 1000) }, 10 * 1000)

View File

@@ -178,6 +178,18 @@ const file = {
mainWindow.webContents.send('list:isMarkdownNote', 'print') mainWindow.webContents.send('list:isMarkdownNote', 'print')
mainWindow.webContents.send('print') mainWindow.webContents.send('print')
} }
},
{
type: 'separator'
},
{
label: 'Update',
click() {
mainWindow.webContents.send('update')
}
},
{
type: 'separator'
} }
] ]
} }
@@ -472,9 +484,21 @@ const help = {
] ]
} }
const team = {
label: 'For Team',
submenu: [
{
label: 'BoostHub',
click: async () => {
shell.openExternal('https://boosthub.io/')
}
}
]
}
module.exports = module.exports =
process.platform === 'darwin' process.platform === 'darwin'
? [boost, file, edit, view, window, help] ? [boost, file, edit, view, window, team, help]
: process.platform === 'win32' : process.platform === 'win32'
? [boost, file, view, help] ? [boost, file, view, team, help]
: [file, view, help] : [file, view, team, help]

View File

@@ -72,7 +72,7 @@
border-left-color: rgba(142, 142, 142, 0.5); border-left-color: rgba(142, 142, 142, 0.5);
mix-blend-mode: difference; mix-blend-mode: difference;
} }
.CodeMirror-scroll { .CodeMirror-scroll {
margin-bottom: 0; margin-bottom: 0;
padding-bottom: 0; padding-bottom: 0;
@@ -116,7 +116,7 @@
<script src="../extra_scripts/codemirror/mode/gfm/gfm.js"></script> <script src="../extra_scripts/codemirror/mode/gfm/gfm.js"></script>
<script src="../extra_scripts/codemirror/addon/hyperlink/hyperlink.js"></script> <script src="../extra_scripts/codemirror/addon/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/edit/matchbrackets.js"></script>
<script src="../node_modules/codemirror/addon/search/search.js"></script> <script src="../node_modules/codemirror/addon/search/search.js"></script>

View File

@@ -112,7 +112,7 @@
<script src="../extra_scripts/codemirror/mode/gfm/gfm.js"></script> <script src="../extra_scripts/codemirror/mode/gfm/gfm.js"></script>
<script src="../extra_scripts/codemirror/addon/hyperlink/hyperlink.js"></script> <script src="../extra_scripts/codemirror/addon/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/edit/matchbrackets.js"></script>
<script src="../node_modules/codemirror/addon/search/search.js"></script> <script src="../node_modules/codemirror/addon/search/search.js"></script>

View File

@@ -202,7 +202,6 @@
"Create new folder": "Ordner erstellen", "Create new folder": "Ordner erstellen",
"Folder name": "Ordnername", "Folder name": "Ordnername",
"Create": "Erstellen", "Create": "Erstellen",
"Untitled": "Neuer Ordner",
"Unlink Storage": "Speicherverknüpfung aufheben", "Unlink Storage": "Speicherverknüpfung aufheben",
"Unlinking removes this linked storage from Boostnote. No data is removed, please manually delete the folder from your hard drive if needed.": "Die Verknüpfung des Speichers mit Boostnote wird entfernt. Es werden keine Daten gelöscht. Um die Daten dauerhaft zu löschen musst du den Ordner auf der Festplatte manuell entfernen.", "Unlinking removes this linked storage from Boostnote. No data is removed, please manually delete the folder from your hard drive if needed.": "Die Verknüpfung des Speichers mit Boostnote wird entfernt. Es werden keine Daten gelöscht. Um die Daten dauerhaft zu löschen musst du den Ordner auf der Festplatte manuell entfernen.",
"Empty note": "Leere Notiz", "Empty note": "Leere Notiz",

View File

@@ -1,7 +1,7 @@
{ {
"name": "boost", "name": "boost",
"productName": "Boostnote", "productName": "Boostnote",
"version": "0.15.3", "version": "0.16.1",
"main": "index.js", "main": "index.js",
"description": "Boostnote", "description": "Boostnote",
"license": "GPL-3.0", "license": "GPL-3.0",
@@ -61,6 +61,7 @@
"chart.js": "^2.7.2", "chart.js": "^2.7.2",
"codemirror": "^5.40.2", "codemirror": "^5.40.2",
"codemirror-mode-elixir": "^1.1.1", "codemirror-mode-elixir": "^1.1.1",
"command-exists": "^1.2.9",
"connected-react-router": "^6.4.0", "connected-react-router": "^6.4.0",
"electron-config": "^1.0.0", "electron-config": "^1.0.0",
"electron-gh-releases": "^2.0.4", "electron-gh-releases": "^2.0.4",
@@ -79,7 +80,7 @@
"js-yaml": "^3.13.1", "js-yaml": "^3.13.1",
"jsonlint-mod": "^1.7.4", "jsonlint-mod": "^1.7.4",
"katex": "^0.10.1", "katex": "^0.10.1",
"lodash": "^4.17.13", "lodash": "^4.17.19",
"lodash-move": "^1.1.1", "lodash-move": "^1.1.1",
"markdown-it": "^6.0.1", "markdown-it": "^6.0.1",
"markdown-it-abbr": "^1.0.4", "markdown-it-abbr": "^1.0.4",
@@ -95,7 +96,7 @@
"markdown-it-sup": "^1.0.0", "markdown-it-sup": "^1.0.0",
"markdown-toc": "^1.2.0", "markdown-toc": "^1.2.0",
"mdurl": "^1.0.1", "mdurl": "^1.0.1",
"mermaid": "^8.4.2", "mermaid": "^8.5.2",
"moment": "^2.10.3", "moment": "^2.10.3",
"mousetrap": "^1.6.2", "mousetrap": "^1.6.2",
"mousetrap-global-bind": "^1.1.0", "mousetrap-global-bind": "^1.1.0",

View File

@@ -1,6 +1,5 @@
{ {
"trailingComma": "es5", "singleQuote": true,
"tabWidth": 2,
"semi": false, "semi": false,
"singleQuote": true "jsxSingleQuote": true
} }

View File

@@ -1,10 +1,10 @@
> [We've launched desktop and mobile app of the new Boost Note now.](https://github.com/BoostIO/BoostNote.next) > [We've launched desktop and mobile app of the new Boost Note now.](https://github.com/BoostIO/BoostNote.next)
> ### [Boost Note for Teams](https://hub.boostio.co/) > ### [Boost Note for Teams](https://boosthub.io/)
> >
> We'll launch the clean and simple wiki specially optimized for developers called "Boost Hub" at June 2020! > We've developed a collaborative workspace app called "Boost Hub" for developer teams.
> >
> Boost Hub will aim to be a collaborative wiki tool for teams to centralize and amplify the availability and search ability of both first-party and third-party information. > It's customizable and easy to optimize for your team like rego blocks and even lets you edit documents together in real-time!
![Boostnote app screenshot](./resources/repository/top.png) ![Boostnote app screenshot](./resources/repository/top.png)

View File

@@ -702,14 +702,15 @@ it('should remove the all ":storage" and noteKey references', function() {
' </p>\n' + ' </p>\n' +
' </body>\n' + ' </body>\n' +
'</html>' '</html>'
const actual = systemUnderTest.removeStorageAndNoteReferences( const actual = systemUnderTest.replaceStorageReferences(
testInput, testInput,
noteKey noteKey,
systemUnderTest.DESTINATION_FOLDER
) )
expect(actual).toEqual(expectedOutput) expect(actual).toEqual(expectedOutput)
}) })
it('should make sure that "removeStorageAndNoteReferences" works with markdown content as well', function() { it('should make sure that "replaceStorageReferences" works with markdown content as well', function() {
const noteKey = 'noteKey' const noteKey = 'noteKey'
const testInput = const testInput =
'Test input' + 'Test input' +
@@ -736,9 +737,113 @@ it('should make sure that "removeStorageAndNoteReferences" works with markdown c
systemUnderTest.DESTINATION_FOLDER + systemUnderTest.DESTINATION_FOLDER +
path.posix.sep + path.posix.sep +
'pdf.pdf)' 'pdf.pdf)'
const actual = systemUnderTest.removeStorageAndNoteReferences( const actual = systemUnderTest.replaceStorageReferences(
testInput, testInput,
noteKey noteKey,
systemUnderTest.DESTINATION_FOLDER
)
expect(actual).toEqual(expectedOutput)
})
it('should replace the all ":storage" references', function() {
const storageFolder = systemUnderTest.DESTINATION_FOLDER
const noteKey = 'noteKey'
const testInput =
'<html>\n' +
' <head>\n' +
' //header\n' +
' </head>\n' +
' <body data-theme="default">\n' +
' <h2 data-line="0" id="Headline">Headline</h2>\n' +
' <p data-line="2">\n' +
' <img src=":storage' +
mdurl.encode(path.sep) +
noteKey +
mdurl.encode(path.sep) +
'0.6r4zdgc22xp.png" alt="dummyImage.png" >\n' +
' </p>\n' +
' <p data-line="4">\n' +
' <a href=":storage' +
mdurl.encode(path.sep) +
noteKey +
mdurl.encode(path.sep) +
'0.q2i4iw0fyx.pdf">dummyPDF.pdf</a>\n' +
' </p>\n' +
' <p data-line="6">\n' +
' <img src=":storage' +
mdurl.encode(path.sep) +
noteKey +
mdurl.encode(path.sep) +
'd6c5ee92.jpg" alt="dummyImage2.jpg">\n' +
' </p>\n' +
' </body>\n' +
'</html>'
const expectedOutput =
'<html>\n' +
' <head>\n' +
' //header\n' +
' </head>\n' +
' <body data-theme="default">\n' +
' <h2 data-line="0" id="Headline">Headline</h2>\n' +
' <p data-line="2">\n' +
' <img src="' +
storageFolder +
path.sep +
'0.6r4zdgc22xp.png" alt="dummyImage.png" >\n' +
' </p>\n' +
' <p data-line="4">\n' +
' <a href="' +
storageFolder +
path.sep +
'0.q2i4iw0fyx.pdf">dummyPDF.pdf</a>\n' +
' </p>\n' +
' <p data-line="6">\n' +
' <img src="' +
storageFolder +
path.sep +
'd6c5ee92.jpg" alt="dummyImage2.jpg">\n' +
' </p>\n' +
' </body>\n' +
'</html>'
const actual = systemUnderTest.replaceStorageReferences(
testInput,
noteKey,
systemUnderTest.DESTINATION_FOLDER
)
expect(actual).toEqual(expectedOutput)
})
it('should make sure that "replaceStorageReferences" works with markdown content as well', function() {
const noteKey = 'noteKey'
const testInput =
'Test input' +
'![imageName](' +
systemUnderTest.STORAGE_FOLDER_PLACEHOLDER +
path.win32.sep +
noteKey +
path.win32.sep +
'image.jpg) \n' +
'[pdf](' +
systemUnderTest.STORAGE_FOLDER_PLACEHOLDER +
path.posix.sep +
noteKey +
path.posix.sep +
'pdf.pdf)'
const expectedOutput =
'Test input' +
'![imageName](' +
systemUnderTest.DESTINATION_FOLDER +
path.posix.sep +
'image.jpg) \n' +
'[pdf](' +
systemUnderTest.DESTINATION_FOLDER +
path.posix.sep +
'pdf.pdf)'
const actual = systemUnderTest.replaceStorageReferences(
testInput,
noteKey,
systemUnderTest.DESTINATION_FOLDER
) )
expect(actual).toEqual(expectedOutput) expect(actual).toEqual(expectedOutput)
}) })

View File

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

View File

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

View File

@@ -98,11 +98,11 @@ exports[`Markdown.render() should renders checkboxes 1`] = `
exports[`Markdown.render() should renders codeblock correctly 1`] = ` exports[`Markdown.render() should renders codeblock correctly 1`] = `
"<pre class=\\"code CodeMirror\\" data-line=\\"1\\"> "<pre class=\\"code CodeMirror\\" data-line=\\"1\\">
<span class=\\"filename\\">filename.js</span> <span class=\\"filename\\">filename.js</span>
<span class=\\"lineNumber CodeMirror-gutters\\"><span class=\\"CodeMirror-linenumber\\">2</span></span> <span class=\\"lineNumber CodeMirror-gutters\\"><span class=\\"CodeMirror-linenumber\\">2</span></span>
<code class=\\"js\\">var project = 'boostnote'; <code class=\\"js\\">var project = 'boostnote';
</code> </code>
</pre>" </pre>"
`; `;
exports[`Markdown.render() should renders definition lists correctly 1`] = ` exports[`Markdown.render() should renders definition lists correctly 1`] = `

117
yarn.lock
View File

@@ -1966,6 +1966,11 @@ combined-stream@1.0.6, combined-stream@~1.0.5:
dependencies: dependencies:
delayed-stream "~1.0.0" 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: commander@2:
version "2.16.0" version "2.16.0"
resolved "http://registry.npm.taobao.org/commander/download/commander-2.16.0.tgz#f16390593996ceb4f3eeb020b31d78528f7f8a50" 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-selection "1"
d3-transition "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" version "5.12.0"
resolved "https://registry.yarnpkg.com/d3/-/d3-5.12.0.tgz#0ddeac879c28c882317cd439b495290acd59ab61" resolved "https://registry.yarnpkg.com/d3/-/d3-5.12.0.tgz#0ddeac879c28c882317cd439b495290acd59ab61"
integrity sha512-flYVMoVuhPFHd9zVCe2BxIszUWqBcd5fvQGMNRmSiBrgdnh6Vlruh60RJQTouAK9xPbOB0plxMvBm4MoyODXNg== integrity sha512-flYVMoVuhPFHd9zVCe2BxIszUWqBcd5fvQGMNRmSiBrgdnh6Vlruh60RJQTouAK9xPbOB0plxMvBm4MoyODXNg==
@@ -2626,13 +2668,14 @@ d@1:
dependencies: dependencies:
es5-ext "^0.10.9" es5-ext "^0.10.9"
dagre-d3@dagrejs/dagre-d3: dagre-d3@^0.6.4:
version "0.6.4-pre" version "0.6.4"
resolved "https://codeload.github.com/dagrejs/dagre-d3/tar.gz/e1a00e5cb518f5d2304a35647e024f31d178e55b" resolved "https://registry.yarnpkg.com/dagre-d3/-/dagre-d3-0.6.4.tgz#0728d5ce7f177ca2337df141ceb60fbe6eeb7b29"
integrity sha512-e/6jXeCP7/ptlAM48clmX4xTZc5Ek6T6kagS7Oz2HrYSdqcLZFLqpAfh7ldbZRFfxCZVyh61NEPR08UQRVxJzQ==
dependencies: dependencies:
d3 "^5.12" d3 "^5.14"
dagre "^0.8.4" dagre "^0.8.5"
graphlib "^2.1.7" graphlib "^2.1.8"
lodash "^4.17.15" lodash "^4.17.15"
dagre@^0.8.4: dagre@^0.8.4:
@@ -2643,6 +2686,14 @@ dagre@^0.8.4:
graphlib "^2.1.7" graphlib "^2.1.7"
lodash "^4.17.4" 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: dashdash@^1.12.0:
version "1.14.1" version "1.14.1"
resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" 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" version "1.1.1"
resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0" 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: env-paths@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-1.0.0.tgz#4168133b42bb05c38a35b1ae4397c8298ab369e0" resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-1.0.0.tgz#4168133b42bb05c38a35b1ae4397c8298ab369e0"
@@ -4302,6 +4360,13 @@ graphlib@^2.1.7:
dependencies: dependencies:
lodash "^4.17.5" 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: gray-matter@^2.1.0:
version "2.1.1" version "2.1.1"
resolved "https://registry.yarnpkg.com/gray-matter/-/gray-matter-2.1.1.tgz#3042d9adec2a1ded6a7707a9ed2380f8a17a430e" resolved "https://registry.yarnpkg.com/gray-matter/-/gray-matter-2.1.1.tgz#3042d9adec2a1ded6a7707a9ed2380f8a17a430e"
@@ -4490,7 +4555,7 @@ has@^1.0.1:
dependencies: dependencies:
function-bind "^1.0.2" function-bind "^1.0.2"
he@^1.2.0: he@^1.1.1, he@^1.2.0:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
@@ -4540,8 +4605,9 @@ hooker@~0.2.3:
resolved "https://registry.yarnpkg.com/hooker/-/hooker-0.2.3.tgz#b834f723cc4a242aa65963459df6d984c5d3d959" resolved "https://registry.yarnpkg.com/hooker/-/hooker-0.2.3.tgz#b834f723cc4a242aa65963459df6d984c5d3d959"
hosted-git-info@^2.1.4: hosted-git-info@^2.1.4:
version "2.6.0" version "2.8.9"
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.6.0.tgz#23235b29ab230c576aab0d4f13fc046b0b038222" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==
html-comment-regex@^1.1.0: html-comment-regex@^1.1.0:
version "1.1.1" version "1.1.1"
@@ -6108,15 +6174,10 @@ lodash.uniq@^4.5.0:
version "4.5.0" version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" 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: 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.13" version "4.17.19"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.13.tgz#0bdc3a6adc873d2f4e0c4bac285df91b64fc7b93" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b"
integrity sha512-vm3/XWXfWtRua0FkUyEHBZy8kCPjErNBT9fJx8Zvs+U6zjqPbTUOpkaoum3O5uiA8sm+yNMHXfYkTUHFoMxFNA== integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==
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@~0.9.2: lodash@~0.9.2:
version "0.9.2" version "0.9.2"
@@ -6400,22 +6461,21 @@ merge@^1.1.3:
resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.1.tgz#38bebf80c3220a8a487b6fcfb3941bb11720c145" resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.1.tgz#38bebf80c3220a8a487b6fcfb3941bb11720c145"
integrity sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ== integrity sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ==
mermaid@^8.4.2: mermaid@^8.5.2:
version "8.4.2" version "8.5.2"
resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-8.4.2.tgz#91d3d8e9541e72eed7a78d0e882db11564fab3bb" resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-8.5.2.tgz#0f1914cda53d4ea5377380e5ce07a38bef2ea7e8"
integrity sha512-vYSCP2u4XkOnjliWz/QIYwvzF/znQAq22vWJJ3YV40SnwV2JQyHblnwwNYXCprkXw7XfwBKDpSNaJ3HP4WfnZw== integrity sha512-I+s+8/RzlazF3dGOhDUfU/ERkUV4zfIlTWb3703jNx+2lfACs+4AdY9ULQaw6BPWzW3gB+XlXFOOX/m/vqujIA==
dependencies: dependencies:
"@braintree/sanitize-url" "^3.1.0" "@braintree/sanitize-url" "^3.1.0"
crypto-random-string "^3.0.1" crypto-random-string "^3.0.1"
d3 "^5.7.0" d3 "^5.7.0"
dagre "^0.8.4" dagre "^0.8.4"
dagre-d3 dagrejs/dagre-d3 dagre-d3 "^0.6.4"
entity-decode "^2.0.2"
graphlib "^2.1.7" graphlib "^2.1.7"
he "^1.2.0" he "^1.2.0"
lodash "^4.17.11"
minify "^4.1.1" minify "^4.1.1"
moment-mini "^2.22.1" moment-mini "^2.22.1"
prettier "^1.18.2"
scope-css "^1.2.1" scope-css "^1.2.1"
methods@~1.1.2: methods@~1.1.2:
@@ -10027,8 +10087,9 @@ websocket-driver@>=0.5.1:
websocket-extensions ">=0.1.1" websocket-extensions ">=0.1.1"
websocket-extensions@>=0.1.1: websocket-extensions@>=0.1.1:
version "0.1.3" version "0.1.4"
resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.3.tgz#5d2ff22977003ec687a4b87073dfbbac146ccf29" 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: well-known-symbols@^1.0.0:
version "1.0.0" version "1.0.0"