diff --git a/browser/components/CodeEditor.js b/browser/components/CodeEditor.js
index faa04e9a..b03de562 100644
--- a/browser/components/CodeEditor.js
+++ b/browser/components/CodeEditor.js
@@ -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
@@ -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) {
@@ -793,9 +847,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 +991,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) {
diff --git a/browser/components/MarkdownEditor.js b/browser/components/MarkdownEditor.js
index 25d1af0d..ed26dc2b 100644
--- a/browser/components/MarkdownEditor.js
+++ b/browser/components/MarkdownEditor.js
@@ -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)
@@ -366,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}
@@ -382,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}
diff --git a/browser/components/MarkdownPreview.js b/browser/components/MarkdownPreview.js
index 7871704e..4d263319 100755
--- a/browser/components/MarkdownPreview.js
+++ b/browser/components/MarkdownPreview.js
@@ -747,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
@@ -794,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]()
diff --git a/browser/components/MarkdownSplitEditor.js b/browser/components/MarkdownSplitEditor.js
index e1a97946..f95c8f48 100644
--- a/browser/components/MarkdownSplitEditor.js
+++ b/browser/components/MarkdownSplitEditor.js
@@ -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,
@@ -261,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}
@@ -274,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}
/>
@@ -311,7 +481,7 @@ 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}
diff --git a/browser/components/render/MermaidRender.js b/browser/components/render/MermaidRender.js
index 8fd4a102..4f0e774a 100644
--- a/browser/components/render/MermaidRender.js
+++ b/browser/components/render/MermaidRender.js
@@ -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
diff --git a/browser/lib/wakatime-plugin.js b/browser/lib/wakatime-plugin.js
new file mode 100644
index 00000000..9b1233df
--- /dev/null
+++ b/browser/lib/wakatime-plugin.js
@@ -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 }
diff --git a/browser/main/Detail/SnippetNoteDetail.js b/browser/main/Detail/SnippetNoteDetail.js
index 82723092..3cf1a5ce 100644
--- a/browser/main/Detail/SnippetNoteDetail.js
+++ b/browser/main/Detail/SnippetNoteDetail.js
@@ -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}
/>
)}
diff --git a/browser/main/lib/ConfigManager.js b/browser/main/lib/ConfigManager.js
index 385caeb5..4356fd01 100644
--- a/browser/main/lib/ConfigManager.js
+++ b/browser/main/lib/ConfigManager.js
@@ -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,
@@ -143,7 +149,10 @@ export const DEFAULT_CONFIG = {
variable: 'boostnote',
prefixAttachmentFolder: false
},
- coloredTags: {}
+ coloredTags: {},
+ wakatime: {
+ key: null
+ }
}
function validate(config) {
@@ -259,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,
diff --git a/browser/main/lib/ThemeManager.js b/browser/main/lib/ThemeManager.js
index a1b090e9..599a61f2 100644
--- a/browser/main/lib/ThemeManager.js
+++ b/browser/main/lib/ThemeManager.js
@@ -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
diff --git a/browser/main/modals/PreferencesModal/ConfigTab.styl b/browser/main/modals/PreferencesModal/ConfigTab.styl
index c27bd3ac..a2d4901f 100644
--- a/browser/main/modals/PreferencesModal/ConfigTab.styl
+++ b/browser/main/modals/PreferencesModal/ConfigTab.styl
@@ -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
diff --git a/browser/main/modals/PreferencesModal/PluginsTab.js b/browser/main/modals/PreferencesModal/PluginsTab.js
new file mode 100644
index 00000000..ceaa383a
--- /dev/null
+++ b/browser/main/modals/PreferencesModal/PluginsTab.js
@@ -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 ? (
+
{pluginsAlert.message}
+ ) : null
+
+ const wakatimeAlert = this.state.wakatimePluginAlert
+ const wakatimePluginAlertElement =
+ wakatimeAlert != null ? (
+ {wakatimeAlert.message}
+ ) : null
+
+ const { config } = this.state
+
+ return (
+
+
+
{i18n.__('Plugins')}
+
{i18n.__('Wakatime')}
+
+
+
+
+
{i18n.__('Wakatime key')}
+
+ this.handleWakatimeKeyChange(e)}
+ disabled={!config.wakatime.isActive}
+ ref='wakatimeKey'
+ value={config.wakatime.key}
+ type='text'
+ />
+ {wakatimePluginAlertElement}
+
+
+
+
+ {pluginsAlertElement}
+
+
+
+ )
+ }
+}
+
+PluginsTab.propTypes = {
+ dispatch: PropTypes.func,
+ haveToSave: PropTypes.func
+}
+
+export default CSSModules(PluginsTab, styles)
diff --git a/browser/main/modals/PreferencesModal/SnippetEditor.js b/browser/main/modals/PreferencesModal/SnippetEditor.js
index f748924c..3a5eb837 100644
--- a/browser/main/modals/PreferencesModal/SnippetEditor.js
+++ b/browser/main/modals/PreferencesModal/SnippetEditor.js
@@ -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'
})
diff --git a/browser/main/modals/PreferencesModal/SnippetTab.js b/browser/main/modals/PreferencesModal/SnippetTab.js
index 0476c5c2..156b4424 100644
--- a/browser/main/modals/PreferencesModal/SnippetTab.js
+++ b/browser/main/modals/PreferencesModal/SnippetTab.js
@@ -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
diff --git a/browser/main/modals/PreferencesModal/UiTab.js b/browser/main/modals/PreferencesModal/UiTab.js
index 17eb5558..e4e96340 100644
--- a/browser/main/modals/PreferencesModal/UiTab.js
+++ b/browser/main/modals/PreferencesModal/UiTab.js
@@ -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 {
+
+
+ {i18n.__('Matching character pairs')}
+
+
+ this.handleUIChange(e)}
+ type='text'
+ />
+
+
+
+
+
+ {i18n.__('in code blocks')}
+
+
+ this.handleUIChange(e)}
+ type='text'
+ />
+
+
+
+
+
+ {i18n.__('Close pairs before')}
+
+
+ this.handleUIChange(e)}
+ type='text'
+ />
+
+
+
+
+
+ {i18n.__('in code blocks')}
+
+
+ this.handleUIChange(e)}
+ type='text'
+ />
+
+
+
+
+
+ {i18n.__('Matching character triples')}
+
+
+ this.handleUIChange(e)}
+ type='text'
+ />
+
+
+
+
+
+ {i18n.__('in code blocks')}
+
+
+ this.handleUIChange(e)}
+ type='text'
+ />
+
+
+
+
+
+ {i18n.__('Exploding character pairs')}
+
+
+ this.handleUIChange(e)}
+ type='text'
+ />
+
+
+
+
+
+ {i18n.__('in code blocks')}
+
+
+ this.handleUIChange(e)}
+ type='text'
+ />
+
+
+
-
-
- {i18n.__('Matching character pairs')}
-
-
+
+
+
+ {i18n.__('Date shortcut use iso 8601 format')}
+
-
-
- {i18n.__('Matching character triples')}
-
-
- this.handleUIChange(e)}
- type='text'
- />
-
-
-
-
-
- {i18n.__('Exploding character pairs')}
-
-
- this.handleUIChange(e)}
- type='text'
- />
-
-
{i18n.__('Custom MarkdownLint Rules')}
diff --git a/browser/main/modals/PreferencesModal/index.js b/browser/main/modals/PreferencesModal/index.js
index e217d3fb..80062e59 100644
--- a/browser/main/modals/PreferencesModal/index.js
+++ b/browser/main/modals/PreferencesModal/index.js
@@ -8,6 +8,7 @@ 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'
@@ -93,6 +94,14 @@ class Preferences extends React.Component {
)
case 'SNIPPET':
return
+ case 'PLUGINS':
+ return (
+
this.setState({ PluginsAlert: alert })}
+ />
+ )
case 'STORAGES':
default:
return (
@@ -138,7 +147,8 @@ class Preferences extends React.Component {
label: i18n.__('Export'),
Export: this.state.ExportAlert
},
- { target: 'SNIPPET', label: i18n.__('Snippets') }
+ { target: 'SNIPPET', label: i18n.__('Snippets') },
+ { target: 'PLUGINS', label: i18n.__('Plugins') }
]
const navButtons = tabs.map(tab => {
diff --git a/extra_scripts/codemirror/addon/edit/closebrackets.js b/extra_scripts/codemirror/addon/edit/closebrackets.js
new file mode 100644
index 00000000..357185ca
--- /dev/null
+++ b/extra_scripts/codemirror/addon/edit/closebrackets.js
@@ -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)))
+ }
+});
\ No newline at end of file
diff --git a/extra_scripts/codemirror/mode/bfm/bfm.js b/extra_scripts/codemirror/mode/bfm/bfm.js
index d08183cd..76d06336 100644
--- a/extra_scripts/codemirror/mode/bfm/bfm.js
+++ b/extra_scripts/codemirror/mode/bfm/bfm.js
@@ -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'
})
-})
+})
\ No newline at end of file
diff --git a/lib/main.development.html b/lib/main.development.html
index 900c66c7..d6216b7e 100644
--- a/lib/main.development.html
+++ b/lib/main.development.html
@@ -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 @@
-
+
diff --git a/lib/main.production.html b/lib/main.production.html
index 05d80345..289fe1b3 100644
--- a/lib/main.production.html
+++ b/lib/main.production.html
@@ -112,7 +112,7 @@
-
+
diff --git a/package.json b/package.json
index 0682c1bb..259742a2 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "boost",
"productName": "Boostnote",
- "version": "0.15.3",
+ "version": "0.16.0",
"main": "index.js",
"description": "Boostnote",
"license": "GPL-3.0",
@@ -61,6 +61,7 @@
"chart.js": "^2.7.2",
"codemirror": "^5.40.2",
"codemirror-mode-elixir": "^1.1.1",
+ "command-exists": "^1.2.9",
"connected-react-router": "^6.4.0",
"electron-config": "^1.0.0",
"electron-gh-releases": "^2.0.4",
@@ -79,7 +80,7 @@
"js-yaml": "^3.13.1",
"jsonlint-mod": "^1.7.4",
"katex": "^0.10.1",
- "lodash": "^4.17.13",
+ "lodash": "^4.17.19",
"lodash-move": "^1.1.1",
"markdown-it": "^6.0.1",
"markdown-it-abbr": "^1.0.4",
@@ -95,7 +96,7 @@
"markdown-it-sup": "^1.0.0",
"markdown-toc": "^1.2.0",
"mdurl": "^1.0.1",
- "mermaid": "^8.4.2",
+ "mermaid": "^8.5.2",
"moment": "^2.10.3",
"mousetrap": "^1.6.2",
"mousetrap-global-bind": "^1.1.0",
diff --git a/prettier.config b/prettier.config
index 66e7e941..515c6cd5 100644
--- a/prettier.config
+++ b/prettier.config
@@ -1,6 +1,5 @@
{
- "trailingComma": "es5",
- "tabWidth": 2,
+ "singleQuote": true,
"semi": false,
- "singleQuote": true
+ "jsxSingleQuote": true
}
\ No newline at end of file
diff --git a/readme.md b/readme.md
index 63c78f19..dffd9676 100644
--- a/readme.md
+++ b/readme.md
@@ -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!

@@ -53,6 +53,10 @@ Issues on Boostnote can be funded by anyone and the money will be distributed to
- [Blog](https://medium.com/boostnote)
- [Reddit](https://www.reddit.com/r/Boostnote/)
+### Boostnote mobile
+A community project developing a mobile cross-platform version of boostnote for iOS and Android can be found here: [NoteApp](https://github.com/T0M0F/NoteApp)
+
+
#### More Information
- Website: https://boostnote.io
diff --git a/yarn.lock b/yarn.lock
index 27221ff9..df0fdf70 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1966,6 +1966,11 @@ combined-stream@1.0.6, combined-stream@~1.0.5:
dependencies:
delayed-stream "~1.0.0"
+command-exists@^1.2.9:
+ version "1.2.9"
+ resolved "https://registry.yarnpkg.com/command-exists/-/command-exists-1.2.9.tgz#c50725af3808c8ab0260fd60b01fbfa25b954f69"
+ integrity sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==
+
commander@2:
version "2.16.0"
resolved "http://registry.npm.taobao.org/commander/download/commander-2.16.0.tgz#f16390593996ceb4f3eeb020b31d78528f7f8a50"
@@ -2583,7 +2588,44 @@ d3-zoom@1:
d3-selection "1"
d3-transition "1"
-d3@^5.12, d3@^5.7.0:
+d3@^5.14:
+ version "5.16.0"
+ resolved "https://registry.yarnpkg.com/d3/-/d3-5.16.0.tgz#9c5e8d3b56403c79d4ed42fbd62f6113f199c877"
+ integrity sha512-4PL5hHaHwX4m7Zr1UapXW23apo6pexCgdetdJ5kTmADpG/7T9Gkxw0M0tf/pjoB63ezCCm0u5UaFYy2aMt0Mcw==
+ dependencies:
+ d3-array "1"
+ d3-axis "1"
+ d3-brush "1"
+ d3-chord "1"
+ d3-collection "1"
+ d3-color "1"
+ d3-contour "1"
+ d3-dispatch "1"
+ d3-drag "1"
+ d3-dsv "1"
+ d3-ease "1"
+ d3-fetch "1"
+ d3-force "1"
+ d3-format "1"
+ d3-geo "1"
+ d3-hierarchy "1"
+ d3-interpolate "1"
+ d3-path "1"
+ d3-polygon "1"
+ d3-quadtree "1"
+ d3-random "1"
+ d3-scale "2"
+ d3-scale-chromatic "1"
+ d3-selection "1"
+ d3-shape "1"
+ d3-time "1"
+ d3-time-format "2"
+ d3-timer "1"
+ d3-transition "1"
+ d3-voronoi "1"
+ d3-zoom "1"
+
+d3@^5.7.0:
version "5.12.0"
resolved "https://registry.yarnpkg.com/d3/-/d3-5.12.0.tgz#0ddeac879c28c882317cd439b495290acd59ab61"
integrity sha512-flYVMoVuhPFHd9zVCe2BxIszUWqBcd5fvQGMNRmSiBrgdnh6Vlruh60RJQTouAK9xPbOB0plxMvBm4MoyODXNg==
@@ -2626,13 +2668,14 @@ d@1:
dependencies:
es5-ext "^0.10.9"
-dagre-d3@dagrejs/dagre-d3:
- version "0.6.4-pre"
- resolved "https://codeload.github.com/dagrejs/dagre-d3/tar.gz/e1a00e5cb518f5d2304a35647e024f31d178e55b"
+dagre-d3@^0.6.4:
+ version "0.6.4"
+ resolved "https://registry.yarnpkg.com/dagre-d3/-/dagre-d3-0.6.4.tgz#0728d5ce7f177ca2337df141ceb60fbe6eeb7b29"
+ integrity sha512-e/6jXeCP7/ptlAM48clmX4xTZc5Ek6T6kagS7Oz2HrYSdqcLZFLqpAfh7ldbZRFfxCZVyh61NEPR08UQRVxJzQ==
dependencies:
- d3 "^5.12"
- dagre "^0.8.4"
- graphlib "^2.1.7"
+ d3 "^5.14"
+ dagre "^0.8.5"
+ graphlib "^2.1.8"
lodash "^4.17.15"
dagre@^0.8.4:
@@ -2643,6 +2686,14 @@ dagre@^0.8.4:
graphlib "^2.1.7"
lodash "^4.17.4"
+dagre@^0.8.5:
+ version "0.8.5"
+ resolved "https://registry.yarnpkg.com/dagre/-/dagre-0.8.5.tgz#ba30b0055dac12b6c1fcc247817442777d06afee"
+ integrity sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==
+ dependencies:
+ graphlib "^2.1.8"
+ lodash "^4.17.15"
+
dashdash@^1.12.0:
version "1.14.1"
resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
@@ -3130,6 +3181,13 @@ entities@^1.1.1, entities@~1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0"
+entity-decode@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/entity-decode/-/entity-decode-2.0.2.tgz#e4f807e52c3294246e9347d1f2b02b07fd5f92e7"
+ integrity sha512-5CCY/3ci4MC1m2jlumNjWd7VBFt4VfFnmSqSNmVcXq4gxM3Vmarxtt+SvmBnzwLS669MWdVuXboNVj1qN2esVg==
+ dependencies:
+ he "^1.1.1"
+
env-paths@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-1.0.0.tgz#4168133b42bb05c38a35b1ae4397c8298ab369e0"
@@ -4302,6 +4360,13 @@ graphlib@^2.1.7:
dependencies:
lodash "^4.17.5"
+graphlib@^2.1.8:
+ version "2.1.8"
+ resolved "https://registry.yarnpkg.com/graphlib/-/graphlib-2.1.8.tgz#5761d414737870084c92ec7b5dbcb0592c9d35da"
+ integrity sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==
+ dependencies:
+ lodash "^4.17.15"
+
gray-matter@^2.1.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/gray-matter/-/gray-matter-2.1.1.tgz#3042d9adec2a1ded6a7707a9ed2380f8a17a430e"
@@ -4490,7 +4555,7 @@ has@^1.0.1:
dependencies:
function-bind "^1.0.2"
-he@^1.2.0:
+he@^1.1.1, he@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
@@ -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.19, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.5.1, lodash@^4.6.1:
+ version "4.17.19"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b"
+ integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==
lodash@~0.9.2:
version "0.9.2"
@@ -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"