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

Compare commits

...

88 Commits

Author SHA1 Message Date
dependabot[bot]
654f7d2b8d Bump lodash from 4.17.19 to 4.17.21
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.19 to 4.17.21.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.19...4.17.21)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-08 13:31:40 +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 3223 additions and 984 deletions

View File

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

View File

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

View File

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

View File

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

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'
// fixes bad styling in the mermaid dark theme
@@ -61,7 +61,6 @@ function render(element, content, theme, enableHTMLLabel) {
el.setAttribute('ratio', ratio)
el.setAttribute('height', el.parentNode.clientWidth / ratio)
console.log(el)
}
})
} catch (e) {

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -12,6 +12,7 @@ import DevTools from './DevTools'
require('./lib/ipcClient')
require('../lib/customMeta')
import i18n from 'browser/lib/i18n'
import ConfigManager from './lib/ConfigManager'
const electron = require('electron')
@@ -107,6 +108,22 @@ function updateApp() {
}
}
function downloadUpdate() {
const index = dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning',
message: i18n.__('Update Boostnote'),
detail: i18n.__('New Boostnote is ready to be downloaded.'),
buttons: [i18n.__('Download now'), i18n.__('Ignore updates')]
})
if (index === 0) {
ipcRenderer.send('update-download-confirm')
} else if (index === 1) {
ipcRenderer.send('update-cancel')
ConfigManager.set({ autoUpdateEnabled: false })
}
}
ReactDOM.render(
<Provider store={store}>
<ConnectedRouter history={history}>
@@ -147,8 +164,12 @@ ReactDOM.render(
})
ipcRenderer.on('update-found', function() {
notify('Update found!', {
body: 'Preparing to update...'
downloadUpdate()
})
ipcRenderer.on('update-not-found', function(_, msg) {
notify('Update not found!', {
body: msg
})
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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'),
deleteNote: require('./deleteNote'),
moveNote: require('./moveNote'),
exportNoteAs: require('./exportNoteAs'),
migrateFromV5Storage: require('./migrateFromV5Storage'),
createSnippet: require('./createSnippet'),
deleteSnippet: require('./deleteSnippet'),
updateSnippet: require('./updateSnippet'),
fetchSnippet: require('./fetchSnippet'),
exportTag: require('./exportTag'),
getFilename: require('./getFilename'),
_migrateFromV6Storage: require('./migrateFromV6Storage'),
_resolveStorageData: require('./resolveStorageData'),

View File

@@ -139,6 +139,13 @@ div[id^="firstRow"]
margin-right 10px
font-size 14px
.group-section-label-right
width 200px
text-align right
margin-right 10px
font-size 14px
padding-right 1.5rem
.group-section-control
flex 1
margin-left 5px

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)
this.state = {
config: this.props.config
config: this.props.config,
subscriptionFormStatus: 'idle',
subscriptionFormErrorMessage: null,
subscriptionFormEmail: ''
}
}
componentDidMount() {
const { autoUpdateEnabled, amaEnabled } = ConfigManager.get()
this.setState({ config: { autoUpdateEnabled, amaEnabled } })
}
handleLinkClick(e) {
shell.openExternal(e.currentTarget.href)
e.preventDefault()
}
handleConfigChange(e) {
const newConfig = { amaEnabled: this.refs.amaEnabled.checked }
const newConfig = {
amaEnabled: this.refs.amaEnabled.checked,
autoUpdateEnabled: this.refs.autoUpdateEnabled.checked
}
this.setState({ config: newConfig })
return newConfig
}
handleSubscriptionFormSubmit(e) {
e.preventDefault()
this.setState({
subscriptionFormStatus: 'sending',
subscriptionFormErrorMessage: null
})
fetch(
'https://boostmails.boostio.co/api/public/lists/5f434dccd05f3160b41c0d49/subscriptions',
{
headers: {
Accept: 'application/json',
'Content-Type': 'application/json'
},
method: 'POST',
body: JSON.stringify({ email: this.state.subscriptionFormEmail })
}
)
.then(response => {
if (response.status >= 400) {
return response.text().then(text => {
throw new Error(text)
})
}
this.setState({
subscriptionFormStatus: 'done'
})
})
.catch(error => {
this.setState({
subscriptionFormStatus: 'idle',
subscriptionFormErrorMessage: error.message
})
})
}
handleSubscriptionFormEmailChange(e) {
this.setState({
subscriptionFormEmail: e.target.value
})
}
handleSaveButtonClick(e) {
const newConfig = {
amaEnabled: this.state.config.amaEnabled
}
const newConfig = this.state.config
if (!newConfig.amaEnabled) {
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('DISABLE_AMA')
@@ -61,20 +114,17 @@ class InfoTab extends React.Component {
})
}
toggleAutoUpdate() {
const newConfig = {
autoUpdateEnabled: !this.state.config.autoUpdateEnabled
}
this.setState({ config: newConfig })
ConfigManager.set(newConfig)
}
infoMessage() {
const { amaMessage } = this.state
return amaMessage ? <p styleName='policy-confirm'>{amaMessage}</p> : null
}
handleAutoUpdateChange() {
const { autoUpdateEnabled } = this.handleConfigChange()
ConfigManager.set({ autoUpdateEnabled })
}
render() {
return (
<div styleName='root'>
@@ -134,6 +184,40 @@ class InfoTab extends React.Component {
<hr />
<div styleName='group-header--sub'>Subscribe Update Notes</div>
{this.state.subscriptionFormStatus === 'done' ? (
<div>
<blockquote color={{ color: 'green' }}>
Thanks for the subscription!
</blockquote>
</div>
) : (
<div>
{this.state.subscriptionFormErrorMessage != null && (
<blockquote style={{ color: 'red' }}>
{this.state.subscriptionFormErrorMessage}
</blockquote>
)}
<form onSubmit={e => this.handleSubscriptionFormSubmit(e)}>
<input
styleName='subscription-email-input'
placeholder='E-mail'
type='email'
onChange={e => this.handleSubscriptionFormEmailChange(e)}
disabled={this.state.subscriptionFormStatus === 'sending'}
/>
<button
styleName='subscription-submit-button'
type='submit'
disabled={this.state.subscriptionFormStatus === 'sending'}
>
Subscribe
</button>
</form>
</div>
)}
<hr />
<div styleName='group-header--sub'>{i18n.__('About')}</div>
<div styleName='top'>
@@ -181,7 +265,8 @@ class InfoTab extends React.Component {
<label>
<input
type='checkbox'
onChange={this.toggleAutoUpdate.bind(this)}
ref='autoUpdateEnabled'
onChange={() => this.handleAutoUpdateChange()}
checked={this.state.config.autoUpdateEnabled}
/>
{i18n.__('Enable Auto Update')}

View File

@@ -33,6 +33,35 @@
.separate-line
margin 40px 0
.subscription-email-input
height 35px
vertical-align middle
width 200px
font-size $tab--button-font-size
border solid 1px $border-color
border-radius 2px
padding 0 5px
margin-right 5px
outline none
&:disabled
background-color $ui-input--disabled-backgroundColor
.subscription-submit-button
margin-top 10px
height 35px
border-radius 2px
border none
background-color alpha(#1EC38B, 90%)
padding-left 20px
padding-right 20px
text-decoration none
color white
font-weight 600
font-size 16px
&:hover
background-color #1EC38B
transition 0.2s
.policy-submit
margin-top 10px
height 35px

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,
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
autoCloseBrackets: {
pairs: this.props.matchingPairs,
triples: this.props.matchingTriples,
explode: this.props.explodingPairs,
override: true
codeBlock: {
pairs: this.props.codeBlockMatchingPairs,
closeBefore: this.props.codeBlockMatchingCloseBefore,
triples: this.props.codeBlockMatchingTriples,
explode: this.props.codeBlockExplodingPairs
},
markdown: {
pairs: this.props.matchingPairs,
closeBefore: this.props.matchingCloseBefore,
triples: this.props.matchingTriples,
explode: this.props.explodingPairs
}
},
mode: 'null'
})

View File

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

View File

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

View File

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

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

View File

@@ -187,7 +187,7 @@ module.exports = function(grunt) {
}
ChildProcess.exec(
`codesign --verbose --deep --force --sign \"${OSX_COMMON_NAME}\" dist/Boostnote-darwin-x64/Boostnote.app`,
`codesign --verbose --deep --force --timestamp=none --sign \"${OSX_COMMON_NAME}\" dist/Boostnote-darwin-x64/Boostnote.app`,
function(err, stdout, stderr) {
grunt.log.writeln(stdout)
if (err) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,5 @@
{
"trailingComma": "es5",
"tabWidth": 2,
"singleQuote": true,
"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)
> ### [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)

View File

@@ -702,14 +702,15 @@ it('should remove the all ":storage" and noteKey references', function() {
' </p>\n' +
' </body>\n' +
'</html>'
const actual = systemUnderTest.removeStorageAndNoteReferences(
const actual = systemUnderTest.replaceStorageReferences(
testInput,
noteKey
noteKey,
systemUnderTest.DESTINATION_FOLDER
)
expect(actual).toEqual(expectedOutput)
})
it('should make sure that "removeStorageAndNoteReferences" works with markdown content as well', function() {
it('should make sure that "replaceStorageReferences" works with markdown content as well', function() {
const noteKey = 'noteKey'
const testInput =
'Test input' +
@@ -736,9 +737,113 @@ it('should make sure that "removeStorageAndNoteReferences" works with markdown c
systemUnderTest.DESTINATION_FOLDER +
path.posix.sep +
'pdf.pdf)'
const actual = systemUnderTest.removeStorageAndNoteReferences(
const actual = systemUnderTest.replaceStorageReferences(
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)
})

View File

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

View File

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

View File

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

112
yarn.lock
View File

@@ -1966,6 +1966,11 @@ combined-stream@1.0.6, combined-stream@~1.0.5:
dependencies:
delayed-stream "~1.0.0"
command-exists@^1.2.9:
version "1.2.9"
resolved "https://registry.yarnpkg.com/command-exists/-/command-exists-1.2.9.tgz#c50725af3808c8ab0260fd60b01fbfa25b954f69"
integrity sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==
commander@2:
version "2.16.0"
resolved "http://registry.npm.taobao.org/commander/download/commander-2.16.0.tgz#f16390593996ceb4f3eeb020b31d78528f7f8a50"
@@ -2583,7 +2588,44 @@ d3-zoom@1:
d3-selection "1"
d3-transition "1"
d3@^5.12, d3@^5.7.0:
d3@^5.14:
version "5.16.0"
resolved "https://registry.yarnpkg.com/d3/-/d3-5.16.0.tgz#9c5e8d3b56403c79d4ed42fbd62f6113f199c877"
integrity sha512-4PL5hHaHwX4m7Zr1UapXW23apo6pexCgdetdJ5kTmADpG/7T9Gkxw0M0tf/pjoB63ezCCm0u5UaFYy2aMt0Mcw==
dependencies:
d3-array "1"
d3-axis "1"
d3-brush "1"
d3-chord "1"
d3-collection "1"
d3-color "1"
d3-contour "1"
d3-dispatch "1"
d3-drag "1"
d3-dsv "1"
d3-ease "1"
d3-fetch "1"
d3-force "1"
d3-format "1"
d3-geo "1"
d3-hierarchy "1"
d3-interpolate "1"
d3-path "1"
d3-polygon "1"
d3-quadtree "1"
d3-random "1"
d3-scale "2"
d3-scale-chromatic "1"
d3-selection "1"
d3-shape "1"
d3-time "1"
d3-time-format "2"
d3-timer "1"
d3-transition "1"
d3-voronoi "1"
d3-zoom "1"
d3@^5.7.0:
version "5.12.0"
resolved "https://registry.yarnpkg.com/d3/-/d3-5.12.0.tgz#0ddeac879c28c882317cd439b495290acd59ab61"
integrity sha512-flYVMoVuhPFHd9zVCe2BxIszUWqBcd5fvQGMNRmSiBrgdnh6Vlruh60RJQTouAK9xPbOB0plxMvBm4MoyODXNg==
@@ -2626,13 +2668,14 @@ d@1:
dependencies:
es5-ext "^0.10.9"
dagre-d3@dagrejs/dagre-d3:
version "0.6.4-pre"
resolved "https://codeload.github.com/dagrejs/dagre-d3/tar.gz/e1a00e5cb518f5d2304a35647e024f31d178e55b"
dagre-d3@^0.6.4:
version "0.6.4"
resolved "https://registry.yarnpkg.com/dagre-d3/-/dagre-d3-0.6.4.tgz#0728d5ce7f177ca2337df141ceb60fbe6eeb7b29"
integrity sha512-e/6jXeCP7/ptlAM48clmX4xTZc5Ek6T6kagS7Oz2HrYSdqcLZFLqpAfh7ldbZRFfxCZVyh61NEPR08UQRVxJzQ==
dependencies:
d3 "^5.12"
dagre "^0.8.4"
graphlib "^2.1.7"
d3 "^5.14"
dagre "^0.8.5"
graphlib "^2.1.8"
lodash "^4.17.15"
dagre@^0.8.4:
@@ -2643,6 +2686,14 @@ dagre@^0.8.4:
graphlib "^2.1.7"
lodash "^4.17.4"
dagre@^0.8.5:
version "0.8.5"
resolved "https://registry.yarnpkg.com/dagre/-/dagre-0.8.5.tgz#ba30b0055dac12b6c1fcc247817442777d06afee"
integrity sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==
dependencies:
graphlib "^2.1.8"
lodash "^4.17.15"
dashdash@^1.12.0:
version "1.14.1"
resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
@@ -3130,6 +3181,13 @@ entities@^1.1.1, entities@~1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0"
entity-decode@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/entity-decode/-/entity-decode-2.0.2.tgz#e4f807e52c3294246e9347d1f2b02b07fd5f92e7"
integrity sha512-5CCY/3ci4MC1m2jlumNjWd7VBFt4VfFnmSqSNmVcXq4gxM3Vmarxtt+SvmBnzwLS669MWdVuXboNVj1qN2esVg==
dependencies:
he "^1.1.1"
env-paths@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-1.0.0.tgz#4168133b42bb05c38a35b1ae4397c8298ab369e0"
@@ -4302,6 +4360,13 @@ graphlib@^2.1.7:
dependencies:
lodash "^4.17.5"
graphlib@^2.1.8:
version "2.1.8"
resolved "https://registry.yarnpkg.com/graphlib/-/graphlib-2.1.8.tgz#5761d414737870084c92ec7b5dbcb0592c9d35da"
integrity sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==
dependencies:
lodash "^4.17.15"
gray-matter@^2.1.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/gray-matter/-/gray-matter-2.1.1.tgz#3042d9adec2a1ded6a7707a9ed2380f8a17a430e"
@@ -4490,7 +4555,7 @@ has@^1.0.1:
dependencies:
function-bind "^1.0.2"
he@^1.2.0:
he@^1.1.1, he@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
@@ -6108,15 +6173,10 @@ lodash.uniq@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
lodash@^4.0.0, lodash@^4.0.1, lodash@^4.12.0, lodash@^4.13.1, lodash@^4.16.6, lodash@^4.17.10, lodash@^4.17.13, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.5.1, lodash@^4.6.1:
version "4.17.13"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.13.tgz#0bdc3a6adc873d2f4e0c4bac285df91b64fc7b93"
integrity sha512-vm3/XWXfWtRua0FkUyEHBZy8kCPjErNBT9fJx8Zvs+U6zjqPbTUOpkaoum3O5uiA8sm+yNMHXfYkTUHFoMxFNA==
lodash@^4.13.0, lodash@^4.17.11, lodash@^4.17.15:
version "4.17.15"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
lodash@^4.0.0, lodash@^4.0.1, lodash@^4.12.0, lodash@^4.13.0, lodash@^4.13.1, lodash@^4.16.6, lodash@^4.17.10, lodash@^4.17.15, lodash@^4.17.2, lodash@^4.17.21, 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.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
lodash@~0.9.2:
version "0.9.2"
@@ -6400,22 +6460,21 @@ merge@^1.1.3:
resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.1.tgz#38bebf80c3220a8a487b6fcfb3941bb11720c145"
integrity sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ==
mermaid@^8.4.2:
version "8.4.2"
resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-8.4.2.tgz#91d3d8e9541e72eed7a78d0e882db11564fab3bb"
integrity sha512-vYSCP2u4XkOnjliWz/QIYwvzF/znQAq22vWJJ3YV40SnwV2JQyHblnwwNYXCprkXw7XfwBKDpSNaJ3HP4WfnZw==
mermaid@^8.5.2:
version "8.5.2"
resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-8.5.2.tgz#0f1914cda53d4ea5377380e5ce07a38bef2ea7e8"
integrity sha512-I+s+8/RzlazF3dGOhDUfU/ERkUV4zfIlTWb3703jNx+2lfACs+4AdY9ULQaw6BPWzW3gB+XlXFOOX/m/vqujIA==
dependencies:
"@braintree/sanitize-url" "^3.1.0"
crypto-random-string "^3.0.1"
d3 "^5.7.0"
dagre "^0.8.4"
dagre-d3 dagrejs/dagre-d3
dagre-d3 "^0.6.4"
entity-decode "^2.0.2"
graphlib "^2.1.7"
he "^1.2.0"
lodash "^4.17.11"
minify "^4.1.1"
moment-mini "^2.22.1"
prettier "^1.18.2"
scope-css "^1.2.1"
methods@~1.1.2:
@@ -10027,8 +10086,9 @@ websocket-driver@>=0.5.1:
websocket-extensions ">=0.1.1"
websocket-extensions@>=0.1.1:
version "0.1.3"
resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.3.tgz#5d2ff22977003ec687a4b87073dfbbac146ccf29"
version "0.1.4"
resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42"
integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==
well-known-symbols@^1.0.0:
version "1.0.0"