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! ![Boostnote app screenshot](./resources/repository/top.png) @@ -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"