diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 00000000..a742a59e
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,41 @@
+{
+ // Use IntelliSense to learn about possible attributes.
+ // Hover to view descriptions of existing attributes.
+ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+ "version": "0.2.0",
+ "configurations": [
+
+ {
+ "type": "node",
+ "request": "launch",
+ "name": "BoostNote Main",
+ "protocol": "inspector",
+ "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron",
+ "runtimeArgs": [
+ "--remote-debugging-port=9223",
+ "--hot",
+ "${workspaceFolder}/index.js"
+ ],
+ "windows": {
+ "runtimeExecutable": "${workspaceFolder}/node_modeules/.bin/electron.cmd"
+ }
+ },
+ {
+ "type": "chrome",
+ "request": "attach",
+ "name": "BoostNote Renderer",
+ "port": 9223,
+ "webRoot": "${workspaceFolder}",
+ "sourceMapPathOverrides": {
+ "webpack:///./~/*": "${webRoot}/node_modules/*",
+ "webpack:///*": "${webRoot}/*"
+ }
+ }
+ ],
+ "compounds": [
+ {
+ "name": "BostNote All",
+ "configurations": ["BoostNote Main", "BoostNote Renderer"]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
new file mode 100644
index 00000000..c6664225
--- /dev/null
+++ b/.vscode/tasks.json
@@ -0,0 +1,27 @@
+{
+ // See https://go.microsoft.com/fwlink/?LinkId=733558
+ // for the documentation about the tasks.json format
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "label": "Build Boostnote",
+ "group": "build",
+ "type": "npm",
+ "script": "watch",
+ "isBackground": true,
+ "presentation": {
+ "reveal": "always",
+ },
+ "problemMatcher": {
+ "pattern":[
+ {
+ "regexp": "^([^\\\\s].*)\\\\((\\\\d+,\\\\d+)\\\\):\\\\s*(.*)$",
+ "file": 1,
+ "location": 2,
+ "message": 3
+ }
+ ]
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md
index 1f5ada57..5554c4b8 100644
--- a/ISSUE_TEMPLATE.md
+++ b/ISSUE_TEMPLATE.md
@@ -1,15 +1,25 @@
# Current behavior
# Expected behavior
+
+
# Steps to reproduce
+
+
1.
2.
3.
diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 00000000..58df576a
--- /dev/null
+++ b/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,36 @@
+
+## Description
+
+
+## Issue fixed
+
+
+
+## Type of changes
+
+- :white_circle: Bug fix (Change that fixed an issue)
+- :white_circle: Breaking change (Change that can cause existing functionality to change)
+- :white_circle: Improvement (Change that improves the code. Maybe performance or development improvement)
+- :white_circle: Feature (Change that adds new functionality)
+- :white_circle: Documentation change (Change that modifies documentation. Maybe typo fixes)
+
+## Checklist:
+
+- :white_circle: My code follows [the project code style](docs/code_style.md)
+- :white_circle: I have written test for my code and it has been tested
+- :white_circle: All existing tests have been passed
+- :white_circle: I have attached a screenshot/video to visualize my change if possible
diff --git a/browser/components/CodeEditor.js b/browser/components/CodeEditor.js
index c36a50c1..7719ed90 100644
--- a/browser/components/CodeEditor.js
+++ b/browser/components/CodeEditor.js
@@ -11,15 +11,24 @@ import eventEmitter from 'browser/main/lib/eventEmitter'
import iconv from 'iconv-lite'
import crypto from 'crypto'
import consts from 'browser/lib/consts'
+import styles from '../components/CodeEditor.styl'
import fs from 'fs'
-const { ipcRenderer } = require('electron')
+const { ipcRenderer, remote, clipboard } = require('electron')
import normalizeEditorFontFamily from 'browser/lib/normalizeEditorFontFamily'
+const spellcheck = require('browser/lib/spellcheck')
+const buildEditorContextMenu = require('browser/lib/contextMenuBuilder')
+import TurndownService from 'turndown'
+import { gfm } from 'turndown-plugin-gfm'
CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js'
const buildCMRulers = (rulers, enableRulers) =>
(enableRulers ? rulers.map(ruler => ({ column: ruler })) : [])
+function translateHotkey (hotkey) {
+ return hotkey.replace(/\s*\+\s*/g, '-').replace(/Command/g, 'Cmd').replace(/Control/g, 'Ctrl')
+}
+
export default class CodeEditor extends React.Component {
constructor (props) {
super(props)
@@ -28,7 +37,7 @@ export default class CodeEditor extends React.Component {
leading: false,
trailing: true
})
- this.changeHandler = e => this.handleChange(e)
+ this.changeHandler = (editor, changeObject) => this.handleChange(editor, changeObject)
this.focusHandler = () => {
ipcRenderer.send('editor:focused', true)
}
@@ -51,15 +60,32 @@ export default class CodeEditor extends React.Component {
noteKey
)
}
- this.pasteHandler = (editor, e) => this.handlePaste(editor, e)
+ this.pasteHandler = (editor, e) => {
+ e.preventDefault()
+
+ this.handlePaste(editor, false)
+ }
this.loadStyleHandler = e => {
this.editor.refresh()
}
this.searchHandler = (e, msg) => this.handleSearch(msg)
this.searchState = null
+ this.scrollToLineHandeler = this.scrollToLine.bind(this)
this.formatTable = () => this.handleFormatTable()
+
+ if (props.switchPreview !== 'RIGHTCLICK') {
+ this.contextMenuHandler = function (editor, event) {
+ const menu = buildEditorContextMenu(editor, event)
+ if (menu != null) {
+ setTimeout(() => menu.popup(remote.getCurrentWindow()), 30)
+ }
+ }
+ }
+
this.editorActivityHandler = () => this.handleEditorActivity()
+
+ this.turndownService = new TurndownService()
}
handleSearch (msg) {
@@ -106,42 +132,10 @@ export default class CodeEditor extends React.Component {
}
}
- updateTableEditorState () {
- const active = this.tableEditor.cursorIsInTable(this.tableEditorOptions)
- if (active) {
- if (this.extraKeysMode !== 'editor') {
- this.extraKeysMode = 'editor'
- this.editor.setOption('extraKeys', this.editorKeyMap)
- }
- } else {
- if (this.extraKeysMode !== 'default') {
- this.extraKeysMode = 'default'
- this.editor.setOption('extraKeys', this.defaultKeyMap)
- this.tableEditor.resetSmartCursor()
- }
- }
- }
-
- componentDidMount () {
- const { rulers, enableRulers } = this.props
+ updateDefaultKeyMap () {
+ const { hotkey } = this.props
const expandSnippet = this.expandSnippet.bind(this)
- const defaultSnippet = [
- {
- id: crypto.randomBytes(16).toString('hex'),
- name: 'Dummy text',
- prefix: ['lorem', 'ipsum'],
- content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'
- }
- ]
- if (!fs.existsSync(consts.SNIPPET_FILE)) {
- fs.writeFileSync(
- consts.SNIPPET_FILE,
- JSON.stringify(defaultSnippet, null, 4),
- 'utf8'
- )
- }
-
this.defaultKeyMap = CodeMirror.normalizeKeyMap({
Tab: function (cm) {
const cursor = cm.getCursor()
@@ -192,8 +186,50 @@ export default class CodeEditor extends React.Component {
document.execCommand('copy')
}
return CodeMirror.Pass
+ },
+ [translateHotkey(hotkey.pasteSmartly)]: cm => {
+ this.handlePaste(cm, true)
}
})
+ }
+
+ updateTableEditorState () {
+ const active = this.tableEditor.cursorIsInTable(this.tableEditorOptions)
+ if (active) {
+ if (this.extraKeysMode !== 'editor') {
+ this.extraKeysMode = 'editor'
+ this.editor.setOption('extraKeys', this.editorKeyMap)
+ }
+ } else {
+ if (this.extraKeysMode !== 'default') {
+ this.extraKeysMode = 'default'
+ this.editor.setOption('extraKeys', this.defaultKeyMap)
+ this.tableEditor.resetSmartCursor()
+ }
+ }
+ }
+
+ componentDidMount () {
+ const { rulers, enableRulers } = this.props
+ eventEmitter.on('line:jump', this.scrollToLineHandeler)
+
+ const defaultSnippet = [
+ {
+ id: crypto.randomBytes(16).toString('hex'),
+ name: 'Dummy text',
+ prefix: ['lorem', 'ipsum'],
+ content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'
+ }
+ ]
+ if (!fs.existsSync(consts.SNIPPET_FILE)) {
+ fs.writeFileSync(
+ consts.SNIPPET_FILE,
+ JSON.stringify(defaultSnippet, null, 4),
+ 'utf8'
+ )
+ }
+
+ this.updateDefaultKeyMap()
this.value = this.props.value
this.editor = CodeMirror(this.refs.root, {
@@ -226,6 +262,9 @@ export default class CodeEditor extends React.Component {
this.editor.on('blur', this.blurHandler)
this.editor.on('change', this.changeHandler)
this.editor.on('paste', this.pasteHandler)
+ if (this.props.switchPreview !== 'RIGHTCLICK') {
+ this.editor.on('contextmenu', this.contextMenuHandler)
+ }
eventEmitter.on('top:search', this.searchHandler)
eventEmitter.emit('code:init')
@@ -242,6 +281,10 @@ export default class CodeEditor extends React.Component {
this.textEditorInterface = new TextEditorInterface(this.editor)
this.tableEditor = new TableEditor(this.textEditorInterface)
+ if (this.props.spellCheck) {
+ this.editor.addPanel(this.createSpellCheckPanel(), {position: 'bottom'})
+ }
+
eventEmitter.on('code:format-table', this.formatTable)
this.tableEditorOptions = options({
@@ -311,22 +354,28 @@ export default class CodeEditor extends React.Component {
const snippetLines = snippets[i].content.split('\n')
let cursorLineNumber = 0
let cursorLinePosition = 0
+
+ let cursorIndex
for (let j = 0; j < snippetLines.length; j++) {
- const cursorIndex = snippetLines[j].indexOf(templateCursorString)
+ cursorIndex = snippetLines[j].indexOf(templateCursorString)
+
if (cursorIndex !== -1) {
cursorLineNumber = j
cursorLinePosition = cursorIndex
- cm.replaceRange(
- snippets[i].content.replace(templateCursorString, ''),
- wordBeforeCursor.range.from,
- wordBeforeCursor.range.to
- )
- cm.setCursor({
- line: cursor.line + cursorLineNumber,
- ch: cursorLinePosition
- })
+
+ break
}
}
+
+ cm.replaceRange(
+ snippets[i].content.replace(templateCursorString, ''),
+ wordBeforeCursor.range.from,
+ wordBeforeCursor.range.to
+ )
+ cm.setCursor({
+ line: cursor.line + cursorLineNumber,
+ ch: cursorLinePosition + cursor.ch - wordBeforeCursor.text.length
+ })
} else {
cm.replaceRange(
snippets[i].content,
@@ -383,9 +432,11 @@ 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('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)
}
@@ -445,6 +496,14 @@ export default class CodeEditor extends React.Component {
this.editor.setOption('extraKeys', this.defaultKeyMap)
}
+ if (prevProps.hotkey !== this.props.hotkey) {
+ this.updateDefaultKeyMap()
+
+ if (this.extraKeysMode === 'default') {
+ this.editor.setOption('extraKeys', this.defaultKeyMap)
+ }
+ }
+
if (this.state.clientWidth !== this.refs.root.clientWidth) {
this.setState({
clientWidth: this.refs.root.clientWidth
@@ -453,6 +512,16 @@ export default class CodeEditor extends React.Component {
needRefresh = true
}
+ if (prevProps.spellCheck !== this.props.spellCheck) {
+ if (this.props.spellCheck === false) {
+ spellcheck.setLanguage(this.editor, spellcheck.SPELLCHECK_DISABLED)
+ let elem = document.getElementById('editor-bottom-panel')
+ elem.parentNode.removeChild(elem)
+ } else {
+ this.editor.addPanel(this.createSpellCheckPanel(), {position: 'bottom'})
+ }
+ }
+
if (needRefresh) {
this.editor.refresh()
}
@@ -466,16 +535,23 @@ export default class CodeEditor extends React.Component {
CodeMirror.autoLoadMode(this.editor, syntax.mode)
}
- handleChange (e) {
- this.value = this.editor.getValue()
+ handleChange (editor, changeObject) {
+ spellcheck.handleChange(editor, changeObject)
+ this.value = editor.getValue()
if (this.props.onChange) {
- this.props.onChange(e)
+ this.props.onChange(editor)
}
}
moveCursorTo (row, col) {}
- scrollToLine (num) {}
+ scrollToLine (event, num) {
+ const cursor = {
+ line: num,
+ ch: 1
+ }
+ this.editor.setCursor(cursor)
+ }
focus () {
this.editor.focus()
@@ -516,15 +592,14 @@ export default class CodeEditor extends React.Component {
this.editor.replaceSelection(imageMd)
}
- handlePaste (editor, e) {
- const clipboardData = e.clipboardData
- const { storageKey, noteKey } = this.props
- const dataTransferItem = clipboardData.items[0]
- const pastedTxt = clipboardData.getData('text')
+ handlePaste (editor, forceSmartPaste) {
+ const { storageKey, noteKey, fetchUrlTitle, enableSmartPaste } = this.props
+
const isURL = str => {
const matcher = /^(?:\w+:)?\/\/([^\s\.]+\.\S{2}|localhost[\:?\d]*)\S*$/
return matcher.test(str)
}
+
const isInLinkTag = editor => {
const startCursor = editor.getCursor('start')
const prevChar = editor.getRange(
@@ -538,27 +613,74 @@ export default class CodeEditor extends React.Component {
)
return prevChar === '](' && nextChar === ')'
}
- if (dataTransferItem.type.match('image')) {
- attachmentManagement.handlePastImageEvent(
- this,
- storageKey,
- noteKey,
- dataTransferItem
- )
- } else if (
- this.props.fetchUrlTitle &&
- isURL(pastedTxt) &&
- !isInLinkTag(editor)
- ) {
- this.handlePasteUrl(e, editor, pastedTxt)
+
+ const isInFencedCodeBlock = editor => {
+ const cursor = editor.getCursor()
+
+ let token = editor.getTokenAt(cursor)
+ if (token.state.fencedState) {
+ return true
+ }
+
+ let line = line = cursor.line - 1
+ while (line >= 0) {
+ token = editor.getTokenAt({
+ ch: 3,
+ line
+ })
+
+ if (token.start === token.end) {
+ --line
+ } else if (token.type === 'comment') {
+ if (line > 0) {
+ token = editor.getTokenAt({
+ ch: 3,
+ line: line - 1
+ })
+
+ return token.type !== 'comment'
+ } else {
+ return true
+ }
+ } else {
+ return false
+ }
+ }
+
+ return false
}
- if (attachmentManagement.isAttachmentLink(pastedTxt)) {
+
+ const pastedTxt = clipboard.readText()
+
+ if (isInFencedCodeBlock(editor)) {
+ this.handlePasteText(editor, pastedTxt)
+ } else if (fetchUrlTitle && isURL(pastedTxt) && !isInLinkTag(editor)) {
+ this.handlePasteUrl(editor, pastedTxt)
+ } else if (enableSmartPaste || forceSmartPaste) {
+ const image = clipboard.readImage()
+ if (!image.isEmpty()) {
+ attachmentManagement.handlePastNativeImage(
+ this,
+ storageKey,
+ noteKey,
+ image
+ )
+ } else {
+ const pastedHtml = clipboard.readHTML()
+ if (pastedHtml.length > 0) {
+ this.handlePasteHtml(editor, pastedHtml)
+ } else {
+ this.handlePasteText(editor, pastedTxt)
+ }
+ }
+ } else if (attachmentManagement.isAttachmentLink(pastedTxt)) {
attachmentManagement
.handleAttachmentLinkPaste(storageKey, noteKey, pastedTxt)
.then(modifiedText => {
this.editor.replaceSelection(modifiedText)
})
- e.preventDefault()
+ } else {
+ this.handlePasteText(editor, pastedTxt)
}
}
@@ -568,8 +690,7 @@ export default class CodeEditor extends React.Component {
}
}
- handlePasteUrl (e, editor, pastedTxt) {
- e.preventDefault()
+ handlePasteUrl (editor, pastedTxt) {
const taggedUrl = `<${pastedTxt}>`
editor.replaceSelection(taggedUrl)
@@ -608,6 +729,15 @@ export default class CodeEditor extends React.Component {
})
}
+ handlePasteHtml (editor, pastedHtml) {
+ const markdown = this.turndownService.turndown(pastedHtml)
+ editor.replaceSelection(markdown)
+ }
+
+ handlePasteText (editor, pastedTxt) {
+ editor.replaceSelection(pastedTxt)
+ }
+
mapNormalResponse (response, pastedTxt) {
return this.decodeResponse(response).then(body => {
return new Promise((resolve, reject) => {
@@ -690,6 +820,25 @@ export default class CodeEditor extends React.Component {
/>
)
}
+
+ createSpellCheckPanel () {
+ const panel = document.createElement('div')
+ panel.className = 'panel bottom'
+ panel.id = 'editor-bottom-panel'
+ const dropdown = document.createElement('select')
+ dropdown.title = 'Spellcheck'
+ dropdown.className = styles['spellcheck-select']
+ dropdown.addEventListener('change', (e) => spellcheck.setLanguage(this.editor, dropdown.value))
+ const options = spellcheck.getAvailableDictionaries()
+ for (const op of options) {
+ const option = document.createElement('option')
+ option.value = op.value
+ option.innerHTML = op.label
+ dropdown.appendChild(option)
+ }
+ panel.appendChild(dropdown)
+ return panel
+ }
}
CodeEditor.propTypes = {
@@ -700,7 +849,8 @@ CodeEditor.propTypes = {
className: PropTypes.string,
onBlur: PropTypes.func,
onChange: PropTypes.func,
- readOnly: PropTypes.bool
+ readOnly: PropTypes.bool,
+ spellCheck: PropTypes.bool
}
CodeEditor.defaultProps = {
@@ -710,5 +860,6 @@ CodeEditor.defaultProps = {
fontSize: 14,
fontFamily: 'Monaco, Consolas',
indentSize: 4,
- indentType: 'space'
+ indentType: 'space',
+ spellCheck: false
}
diff --git a/browser/components/CodeEditor.styl b/browser/components/CodeEditor.styl
new file mode 100644
index 00000000..7a254935
--- /dev/null
+++ b/browser/components/CodeEditor.styl
@@ -0,0 +1,6 @@
+.codeEditor-typo
+ text-decoration underline wavy red
+
+.spellcheck-select
+ border: none
+ text-decoration underline wavy red
diff --git a/browser/components/MarkdownEditor.js b/browser/components/MarkdownEditor.js
index 4c195797..d3270c18 100644
--- a/browser/components/MarkdownEditor.js
+++ b/browser/components/MarkdownEditor.js
@@ -6,6 +6,7 @@ import CodeEditor from 'browser/components/CodeEditor'
import MarkdownPreview from 'browser/components/MarkdownPreview'
import eventEmitter from 'browser/main/lib/eventEmitter'
import { findStorage } from 'browser/lib/findStorage'
+import ConfigManager from 'browser/main/lib/ConfigManager'
class MarkdownEditor extends React.Component {
constructor (props) {
@@ -18,7 +19,7 @@ class MarkdownEditor extends React.Component {
this.supportMdSelectionBold = [16, 17, 186]
this.state = {
- status: 'PREVIEW',
+ status: props.config.editor.switchPreview === 'RIGHTCLICK' ? props.config.editor.delfaultStatus : 'PREVIEW',
renderValue: props.value,
keyPressed: new Set(),
isLocked: false
@@ -64,6 +65,10 @@ class MarkdownEditor extends React.Component {
})
}
+ setValue (value) {
+ this.refs.code.setValue(value)
+ }
+
handleChange (e) {
this.value = this.refs.code.value
this.props.onChange(e)
@@ -72,9 +77,7 @@ class MarkdownEditor extends React.Component {
handleContextMenu (e) {
const { config } = this.props
if (config.editor.switchPreview === 'RIGHTCLICK') {
- const newStatus = this.state.status === 'PREVIEW'
- ? 'CODE'
- : 'PREVIEW'
+ const newStatus = this.state.status === 'PREVIEW' ? 'CODE' : 'PREVIEW'
this.setState({
status: newStatus
}, () => {
@@ -84,6 +87,10 @@ class MarkdownEditor extends React.Component {
this.refs.preview.focus()
}
eventEmitter.emit('topbar:togglelockbutton', this.state.status)
+
+ const newConfig = Object.assign({}, config)
+ newConfig.editor.delfaultStatus = newStatus
+ ConfigManager.set(newConfig)
})
}
}
@@ -140,8 +147,10 @@ class MarkdownEditor extends React.Component {
e.preventDefault()
e.stopPropagation()
const idMatch = /checkbox-([0-9]+)/
- const checkedMatch = /\[x\]/i
- const uncheckedMatch = /\[ \]/
+ const checkedMatch = /^\s*[\+\-\*] \[x\]/i
+ const uncheckedMatch = /^\s*[\+\-\*] \[ \]/
+ const checkReplace = /\[x\]/i
+ const uncheckReplace = /\[ \]/
if (idMatch.test(e.target.getAttribute('id'))) {
const lineIndex = parseInt(e.target.getAttribute('id').match(idMatch)[1], 10) - 1
const lines = this.refs.code.value
@@ -150,10 +159,10 @@ class MarkdownEditor extends React.Component {
const targetLine = lines[lineIndex]
if (targetLine.match(checkedMatch)) {
- lines[lineIndex] = targetLine.replace(checkedMatch, '[ ]')
+ lines[lineIndex] = targetLine.replace(checkReplace, '[ ]')
}
if (targetLine.match(uncheckedMatch)) {
- lines[lineIndex] = targetLine.replace(uncheckedMatch, '[x]')
+ lines[lineIndex] = targetLine.replace(uncheckReplace, '[x]')
}
this.refs.code.setValue(lines.join('\n'))
}
@@ -250,7 +259,7 @@ class MarkdownEditor extends React.Component {
: 'codeEditor--hide'
}
ref='code'
- mode='GitHub Flavored Markdown'
+ mode='Boost Flavored Markdown'
value={value}
theme={config.editor.theme}
keyMap={config.editor.keyMap}
@@ -268,6 +277,10 @@ class MarkdownEditor extends React.Component {
enableTableEditor={config.editor.enableTableEditor}
onChange={(e) => this.handleChange(e)}
onBlur={(e) => this.handleBlur(e)}
+ spellCheck={config.editor.spellcheck}
+ enableSmartPaste={config.editor.enableSmartPaste}
+ hotkey={config.hotkey}
+ switchPreview={config.editor.switchPreview}
/>
{percentageOfTodo}%
onClearCheckboxClick(e)}>clear
+${str}`
- }
- if (langType === 'sequence') {
- return `${str}`
- }
- if (langType === 'chart') {
- return `${str}`
- }
- if (langType === 'mermaid') {
- return `${str}`
- }
- return '' +
- '' + fileName + '' +
- createGutter(str, firstLineNumber) +
- '' +
- str +
- ''
- },
sanitize: 'STRICT'
}
@@ -105,7 +80,11 @@ class Markdown {
'iframe': ['src', 'width', 'height', 'frameborder', 'allowfullscreen'],
'input': ['type', 'id', 'checked']
},
- allowedIframeHostnames: ['www.youtube.com']
+ allowedIframeHostnames: ['www.youtube.com'],
+ selfClosing: [ 'img', 'br', 'hr', 'input' ],
+ allowedSchemes: [ 'http', 'https', 'ftp', 'mailto' ],
+ allowedSchemesAppliedToAttributes: [ 'href', 'src', 'cite' ],
+ allowProtocolRelative: true
})
}
@@ -139,19 +118,60 @@ class Markdown {
this.md.use(require('markdown-it-imsize'))
this.md.use(require('markdown-it-footnote'))
this.md.use(require('markdown-it-multimd-table'))
- this.md.use(require('markdown-it-named-headers'), {
- slugify: (header) => {
- return encodeURI(header.trim()
+ this.md.use(anchor, {
+ slugify: (title) => {
+ var slug = encodeURI(title.trim()
.replace(/[\]\[\!\"\#\$\%\&\'\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\_\{\|\}\~]/g, '')
.replace(/\s+/g, '-'))
.replace(/\-+$/, '')
+ return slug
}
})
this.md.use(require('markdown-it-kbd'))
-
this.md.use(require('markdown-it-admonition'), {types: ['note', 'hint', 'attention', 'caution', 'danger', 'error']})
+ this.md.use(require('markdown-it-abbr'))
+ this.md.use(require('markdown-it-sub'))
+ this.md.use(require('markdown-it-sup'))
+ this.md.use(require('./markdown-it-deflist'))
this.md.use(require('./markdown-it-frontmatter'))
+ this.md.use(require('./markdown-it-fence'), {
+ chart: token => {
+ if (token.parameters.hasOwnProperty('yaml')) {
+ token.parameters.format = 'yaml'
+ }
+
+ return `+ ${token.fileName} +` + }, + flowchart: token => { + return `${token.content}+
+ ${token.fileName} +` + }, + mermaid: token => { + return `${token.content}+
+ ${token.fileName} +` + }, + sequence: token => { + return `${token.content}+
+ ${token.fileName} +` + } + }, token => { + return `${token.content}+
+ ${token.fileName}
+ ${createGutter(token.content, token.firstLineNumber)}
+ ${token.content}
+ `
+ })
+
const deflate = require('markdown-it-plantuml/lib/deflate')
this.md.use(require('markdown-it-plantuml'), '', {
generateSource: function (umlCode) {
@@ -223,7 +243,11 @@ class Markdown {
if (!liToken.attrs) {
liToken.attrs = []
}
- liToken.attrs.push(['class', 'taskListItem'])
+ if (config.preview.lineThroughCheckbox) {
+ liToken.attrs.push(['class', `taskListItem${match[1] !== ' ' ? ' checked' : ''}`])
+ } else {
+ liToken.attrs.push(['class', 'taskListItem'])
+ }
}
content = ``
}
@@ -248,9 +272,12 @@ class Markdown {
this.md.renderer.render = (tokens, options, env) => {
tokens.forEach((token) => {
switch (token.type) {
- case 'heading_open':
- case 'paragraph_open':
case 'blockquote_open':
+ case 'dd_open':
+ case 'dt_open':
+ case 'heading_open':
+ case 'list_item_open':
+ case 'paragraph_open':
case 'table_open':
token.attrPush(['data-line', token.map[0]])
}
diff --git a/browser/lib/newNote.js b/browser/lib/newNote.js
index bed69735..0b64d0e1 100644
--- a/browser/lib/newNote.js
+++ b/browser/lib/newNote.js
@@ -3,14 +3,21 @@ import dataApi from 'browser/main/lib/dataApi'
import ee from 'browser/main/lib/eventEmitter'
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
-export function createMarkdownNote (storage, folder, dispatch, location) {
+export function createMarkdownNote (storage, folder, dispatch, location, params, config) {
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_MARKDOWN')
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_ALLNOTE')
+
+ let tags = []
+ if (config.ui.tagNewNoteWithFilteringTags && location.pathname.match(/\/tags/)) {
+ tags = params.tagname.split(' ')
+ }
+
return dataApi
.createNote(storage, {
type: 'MARKDOWN_NOTE',
folder: folder,
title: '',
+ tags,
content: ''
})
.then(note => {
@@ -29,14 +36,21 @@ export function createMarkdownNote (storage, folder, dispatch, location) {
})
}
-export function createSnippetNote (storage, folder, dispatch, location, config) {
+export function createSnippetNote (storage, folder, dispatch, location, params, config) {
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_SNIPPET')
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_ALLNOTE')
+
+ let tags = []
+ if (config.ui.tagNewNoteWithFilteringTags && location.pathname.match(/\/tags/)) {
+ tags = params.tagname.split(' ')
+ }
+
return dataApi
.createNote(storage, {
type: 'SNIPPET_NOTE',
folder: folder,
title: '',
+ tags,
description: '',
snippets: [
{
diff --git a/browser/lib/spellcheck.js b/browser/lib/spellcheck.js
new file mode 100644
index 00000000..dd04e575
--- /dev/null
+++ b/browser/lib/spellcheck.js
@@ -0,0 +1,232 @@
+import styles from '../components/CodeEditor.styl'
+import i18n from 'browser/lib/i18n'
+
+const Typo = require('typo-js')
+const _ = require('lodash')
+
+const CSS_ERROR_CLASS = 'codeEditor-typo'
+const SPELLCHECK_DISABLED = 'NONE'
+const DICTIONARY_PATH = '../dictionaries'
+const MILLISECONDS_TILL_LIVECHECK = 500
+
+let dictionary = null
+let self
+
+function getAvailableDictionaries () {
+ return [
+ {label: i18n.__('Disabled'), value: SPELLCHECK_DISABLED},
+ {label: i18n.__('English'), value: 'en_GB'},
+ {label: i18n.__('German'), value: 'de_DE'},
+ {label: i18n.__('French'), value: 'fr_FR'}
+ ]
+}
+
+/**
+ * Only to be used in the tests :)
+ */
+function setDictionaryForTestsOnly (newDictionary) {
+ dictionary = newDictionary
+}
+
+/**
+ * @description Initializes the spellcheck. It removes all existing marks of the current editor.
+ * If a language was given (i.e. lang !== this.SPELLCHECK_DISABLED) it will load the stated dictionary and use it to check the whole document.
+ * @param {Codemirror} editor CodeMirror-Editor
+ * @param {String} lang on of the values from getAvailableDictionaries()-Method
+ */
+function setLanguage (editor, lang) {
+ self = this
+ dictionary = null
+
+ if (editor == null) {
+ return
+ }
+
+ const existingMarks = editor.getAllMarks() || []
+ for (const mark of existingMarks) {
+ mark.clear()
+ }
+ if (lang !== SPELLCHECK_DISABLED) {
+ dictionary = new Typo(lang, false, false, {
+ dictionaryPath: DICTIONARY_PATH,
+ asyncLoad: true,
+ loadedCallback: () =>
+ checkWholeDocument(editor)
+ })
+ }
+}
+
+/**
+ * Checks the whole content of the editor for typos
+ * @param {Codemirror} editor CodeMirror-Editor
+ */
+function checkWholeDocument (editor) {
+ const lastLine = editor.lineCount() - 1
+ const textOfLastLine = editor.getLine(lastLine) || ''
+ const lastChar = textOfLastLine.length
+ const from = {line: 0, ch: 0}
+ const to = {line: lastLine, ch: lastChar}
+ checkMultiLineRange(editor, from, to)
+}
+
+/**
+ * Checks the given range for typos
+ * @param {Codemirror} editor CodeMirror-Editor
+ * @param {line, ch} from starting position of the spellcheck
+ * @param {line, ch} to end position of the spellcheck
+ */
+function checkMultiLineRange (editor, from, to) {
+ function sortRange (pos1, pos2) {
+ if (pos1.line > pos2.line || (pos1.line === pos2.line && pos1.ch > pos2.ch)) {
+ return {from: pos2, to: pos1}
+ }
+ return {from: pos1, to: pos2}
+ }
+
+ const {from: smallerPos, to: higherPos} = sortRange(from, to)
+ for (let l = smallerPos.line; l <= higherPos.line; l++) {
+ const line = editor.getLine(l) || ''
+ let w = 0
+ if (l === smallerPos.line) {
+ w = smallerPos.ch
+ }
+ let wEnd = line.length
+ if (l === higherPos.line) {
+ wEnd = higherPos.ch
+ }
+ while (w <= wEnd) {
+ const wordRange = editor.findWordAt({line: l, ch: w})
+ self.checkWord(editor, wordRange)
+ w += (wordRange.head.ch - wordRange.anchor.ch) + 1
+ }
+ }
+}
+
+/**
+ * @description Checks whether a certain range of characters in the editor (i.e. a word) contains a typo.
+ * If so the ranged will be marked with the class CSS_ERROR_CLASS.
+ * Note: Due to performance considerations, only words with more then 3 signs are checked.
+ * @param {Codemirror} editor CodeMirror-Editor
+ * @param wordRange Object specifying the range that should be checked.
+ * Having the following structure: {anchor: {line: integer, ch: integer}, head: {line: integer, ch: integer}}
+ */
+function checkWord (editor, wordRange) {
+ const word = editor.getRange(wordRange.anchor, wordRange.head)
+ if (word == null || word.length <= 3) {
+ return
+ }
+ if (!dictionary.check(word)) {
+ editor.markText(wordRange.anchor, wordRange.head, {className: styles[CSS_ERROR_CLASS]})
+ }
+}
+
+/**
+ * Checks the changes recently made (aka live check)
+ * @param {Codemirror} editor CodeMirror-Editor
+ * @param fromChangeObject codeMirror changeObject describing the start of the editing
+ * @param toChangeObject codeMirror changeObject describing the end of the editing
+ */
+function checkChangeRange (editor, fromChangeObject, toChangeObject) {
+ /**
+ * Calculate the smallest respectively largest position as a start, resp. end, position and return it
+ * @param start CodeMirror change object
+ * @param end CodeMirror change object
+ * @returns {{start: {line: *, ch: *}, end: {line: *, ch: *}}}
+ */
+ function getStartAndEnd (start, end) {
+ const possiblePositions = [start.from, start.to, end.from, end.to]
+ let smallest = start.from
+ let biggest = end.to
+ for (const currentPos of possiblePositions) {
+ if (currentPos.line < smallest.line || (currentPos.line === smallest.line && currentPos.ch < smallest.ch)) {
+ smallest = currentPos
+ }
+ if (currentPos.line > biggest.line || (currentPos.line === biggest.line && currentPos.ch > biggest.ch)) {
+ biggest = currentPos
+ }
+ }
+ return {start: smallest, end: biggest}
+ }
+
+ if (dictionary === null || editor == null) { return }
+
+ try {
+ const {start, end} = getStartAndEnd(fromChangeObject, toChangeObject)
+
+ // Expand the range to include words after/before whitespaces
+ start.ch = Math.max(start.ch - 1, 0)
+ end.ch = end.ch + 1
+
+ // clean existing marks
+ const existingMarks = editor.findMarks(start, end) || []
+ for (const mark of existingMarks) {
+ mark.clear()
+ }
+
+ self.checkMultiLineRange(editor, start, end)
+ } catch (e) {
+ console.info('Error during the spell check. It might be due to problems figuring out the range of the new text..', e)
+ }
+}
+
+function saveLiveSpellCheckFrom (changeObject) {
+ liveSpellCheckFrom = changeObject
+}
+let liveSpellCheckFrom
+const debouncedSpellCheckLeading = _.debounce(saveLiveSpellCheckFrom, MILLISECONDS_TILL_LIVECHECK, {
+ 'leading': true,
+ 'trailing': false
+})
+const debouncedSpellCheck = _.debounce(checkChangeRange, MILLISECONDS_TILL_LIVECHECK, {
+ 'leading': false,
+ 'trailing': true
+})
+
+/**
+ * Handles a keystroke. Buffers the input and performs a live spell check after a certain time. Uses _debounce from lodash to buffer the input
+ * @param {Codemirror} editor CodeMirror-Editor
+ * @param changeObject codeMirror changeObject
+ */
+function handleChange (editor, changeObject) {
+ if (dictionary === null) {
+ return
+ }
+ debouncedSpellCheckLeading(changeObject)
+ debouncedSpellCheck(editor, liveSpellCheckFrom, changeObject)
+}
+
+/**
+ * Returns an array of spelling suggestions for the given (wrong written) word.
+ * Returns an empty array if the dictionary is null (=> spellcheck is disabled) or the given word was null
+ * @param word word to be checked
+ * @returns {String[]} Array of suggestions
+ */
+function getSpellingSuggestion (word) {
+ if (dictionary == null || word == null) {
+ return []
+ }
+ return dictionary.suggest(word)
+}
+
+/**
+ * Returns the name of the CSS class used for errors
+ */
+function getCSSClassName () {
+ return styles[CSS_ERROR_CLASS]
+}
+
+module.exports = {
+ DICTIONARY_PATH,
+ CSS_ERROR_CLASS,
+ SPELLCHECK_DISABLED,
+ getAvailableDictionaries,
+ setLanguage,
+ checkChangeRange,
+ handleChange,
+ getSpellingSuggestion,
+ checkWord,
+ checkMultiLineRange,
+ checkWholeDocument,
+ setDictionaryForTestsOnly,
+ getCSSClassName
+}
diff --git a/browser/main/Detail/Detail.styl b/browser/main/Detail/Detail.styl
index 49a634f3..1b7bd606 100644
--- a/browser/main/Detail/Detail.styl
+++ b/browser/main/Detail/Detail.styl
@@ -23,7 +23,7 @@ body[data-theme="dark"]
border-left 1px solid $ui-dark-borderColor
.empty-message
color $ui-dark-inactive-text-color
-
+
body[data-theme="solarized-dark"]
.root
background-color $ui-solarized-dark-noteDetail-backgroundColor
@@ -37,3 +37,10 @@ body[data-theme="monokai"]
border-left 1px solid $ui-monokai-borderColor
.empty-message
color $ui-monokai-text-color
+
+body[data-theme="dracula"]
+ .root
+ background-color $ui-dracula-noteDetail-backgroundColor
+ border-left 1px solid $ui-dracula-borderColor
+ .empty-message
+ color $ui-dracula-text-color
\ No newline at end of file
diff --git a/browser/main/Detail/FolderSelect.styl b/browser/main/Detail/FolderSelect.styl
index cfdc2734..fe045e3a 100644
--- a/browser/main/Detail/FolderSelect.styl
+++ b/browser/main/Detail/FolderSelect.styl
@@ -36,7 +36,7 @@
height 34px
width 20px
line-height 34px
-
+
.search-input
vertical-align middle
position relative
@@ -71,7 +71,7 @@
overflow ellipsis
cursor pointer
&:hover
- background-color $ui-button--hover-backgroundColor
+ background-color $ui-button--hover-backgroundColor
.search-optionList-item--active
@extend .search-optionList-item
@@ -159,3 +159,29 @@ body[data-theme="monokai"]
color $ui-monokai-button--active-color
.search-optionList-item-name-surfix
color $ui-monokai-inactive-text-color
+
+body[data-theme="dracula"]
+ .root
+ color $ui-dracula-text-color
+ &:hover
+ color #f8f8f2
+ background-color $ui-dark-button--hover-backgroundColor
+ border-color $ui-dracula-borderColor
+
+ .search-optionList
+ color #f8f8f2
+ border-color $ui-dracula-borderColor
+ background-color $ui-dracula-button-backgroundColor
+
+ .search-optionList-item
+ &:hover
+ background-color lighten($ui-dracula-button--hover-backgroundColor, 15%)
+
+ .search-optionList-item--active
+ background-color $ui-dracula-button--active-backgroundColor
+ color $ui-dracula-button--active-color
+ &:hover
+ background-color $ui-dark-button--hover-backgroundColor
+ color $ui-dracula-button--active-color
+ .search-optionList-item-name-surfix
+ color $ui-dracula-inactive-text-color
diff --git a/browser/main/Detail/InfoPanel.js b/browser/main/Detail/InfoPanel.js
index 4ce610fa..15535186 100644
--- a/browser/main/Detail/InfoPanel.js
+++ b/browser/main/Detail/InfoPanel.js
@@ -70,22 +70,22 @@ class InfoPanel extends React.Component {
{i18n.__('.md')}
{i18n.__('.txt')}
{i18n.__('.html')}
{i18n.__('Print')}
.md
.txt
.html
{i18n.__('Dear Boostnote users,')}
-{i18n.__('Thank you for using Boostnote!')}
-{i18n.__('Boostnote is used in about 200 different countries and regions by an awesome community of developers.')}
{i18n.__('To support our growing userbase, and satisfy community expectations,')}
-{i18n.__('we would like to invest more time and resources in this project.')}
+{i18n.__('We launched IssueHunt which is an issue-based crowdfunding / sourcing platform for open source projects.')}
+{i18n.__('Anyone can put a bounty on not only a bug but also on OSS feature requests listed on IssueHunt. Collected funds will be distributed to project owners and contributors.')}
{i18n.__('If you use Boostnote and see its potential, help us out by supporting the project on OpenCollective!')}
+{i18n.__('### Sustainable Open Source Ecosystem')}
+{i18n.__('We discussed about open-source ecosystem and IssueHunt concept with the Boostnote team repeatedly. We actually also discussed with Matz who father of Ruby.')}
+{i18n.__('The original reason why we made IssueHunt was to reward our contributors of Boostnote project. We’ve got tons of Github stars and hundred of contributors in two years.')}
+{i18n.__('We thought that it will be nice if we can pay reward for our contributors.')}
{i18n.__('Thanks,')}
+{i18n.__('### We believe Meritocracy')}
+{i18n.__('We think developers who has skill and did great things must be rewarded properly.')}
+{i18n.__('OSS projects are used in everywhere on the internet, but no matter how they great, most of owners of those projects need to have another job to sustain their living.')}
+{i18n.__('It sometimes looks like exploitation.')}
+{i18n.__('We’ve realized IssueHunt could enhance sustainability of open-source ecosystem.')}
+{i18n.__('As same as issues of Boostnote are already funded on IssueHunt, your open-source projects can be also started funding from now.')}
+{i18n.__('Thank you,')}
{i18n.__('The Boostnote Team')}