mirror of
https://github.com/BoostIo/Boostnote
synced 2025-12-13 17:56:25 +00:00
Merge branch 'master' into feature/autoBracketMatching
This commit is contained in:
@@ -15,11 +15,14 @@ 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 } = 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
|
||||
@@ -40,7 +43,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)
|
||||
}
|
||||
@@ -75,7 +78,15 @@ export default class CodeEditor extends React.Component {
|
||||
this.scrollToLineHandeler = this.scrollToLine.bind(this)
|
||||
|
||||
this.formatTable = () => this.handleFormatTable()
|
||||
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) {
|
||||
@@ -246,6 +257,7 @@ 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)
|
||||
this.editor.on('contextmenu', this.contextMenuHandler)
|
||||
eventEmitter.on('top:search', this.searchHandler)
|
||||
|
||||
eventEmitter.emit('code:init')
|
||||
@@ -262,6 +274,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({
|
||||
@@ -405,22 +421,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,
|
||||
@@ -483,9 +505,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)
|
||||
}
|
||||
|
||||
@@ -568,6 +592,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()
|
||||
}
|
||||
@@ -581,10 +615,11 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -841,6 +876,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 = {
|
||||
@@ -851,7 +905,8 @@ CodeEditor.propTypes = {
|
||||
className: PropTypes.string,
|
||||
onBlur: PropTypes.func,
|
||||
onChange: PropTypes.func,
|
||||
readOnly: PropTypes.bool
|
||||
readOnly: PropTypes.bool,
|
||||
spellCheck: PropTypes.bool
|
||||
}
|
||||
|
||||
CodeEditor.defaultProps = {
|
||||
@@ -861,5 +916,6 @@ CodeEditor.defaultProps = {
|
||||
fontSize: 14,
|
||||
fontFamily: 'Monaco, Consolas',
|
||||
indentSize: 4,
|
||||
indentType: 'space'
|
||||
indentType: 'space',
|
||||
spellCheck: false
|
||||
}
|
||||
|
||||
6
browser/components/CodeEditor.styl
Normal file
6
browser/components/CodeEditor.styl
Normal file
@@ -0,0 +1,6 @@
|
||||
.codeEditor-typo
|
||||
text-decoration underline wavy red
|
||||
|
||||
.spellcheck-select
|
||||
border: none
|
||||
text-decoration underline wavy red
|
||||
@@ -147,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
|
||||
@@ -157,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'))
|
||||
}
|
||||
@@ -278,6 +280,7 @@ class MarkdownEditor extends React.Component {
|
||||
enableTableEditor={config.editor.enableTableEditor}
|
||||
onChange={(e) => this.handleChange(e)}
|
||||
onBlur={(e) => this.handleBlur(e)}
|
||||
spellCheck={config.editor.spellcheck}
|
||||
/>
|
||||
<MarkdownPreview styleName={this.state.status === 'PREVIEW'
|
||||
? 'preview'
|
||||
|
||||
@@ -16,9 +16,12 @@ import convertModeName from 'browser/lib/convertModeName'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import mdurl from 'mdurl'
|
||||
import exportNote from 'browser/main/lib/dataApi/exportNote'
|
||||
import { escapeHtmlCharacters } from 'browser/lib/utils'
|
||||
import yaml from 'js-yaml'
|
||||
import context from 'browser/lib/context'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
import fs from 'fs'
|
||||
import ConfigManager from '../main/lib/ConfigManager'
|
||||
|
||||
const { remote, shell } = require('electron')
|
||||
const attachmentManagement = require('../main/lib/dataApi/attachmentManagement')
|
||||
@@ -261,6 +264,10 @@ export default class MarkdownPreview extends React.Component {
|
||||
}
|
||||
|
||||
handleMouseDown (e) {
|
||||
const config = ConfigManager.get()
|
||||
if (config.editor.switchPreview === 'RIGHTCLICK' && e.buttons === 2 && config.editor.type === 'SPLIT') {
|
||||
eventEmitter.emit('topbar:togglemodebutton', 'CODE')
|
||||
}
|
||||
if (e.target != null) {
|
||||
switch (e.target.tagName) {
|
||||
case 'A':
|
||||
@@ -767,7 +774,8 @@ export default class MarkdownPreview extends React.Component {
|
||||
this.refs.root.contentWindow.document.querySelectorAll('.chart'),
|
||||
el => {
|
||||
try {
|
||||
const chartConfig = JSON.parse(el.innerHTML)
|
||||
const format = el.attributes.getNamedItem('data-format').value
|
||||
const chartConfig = format === 'yaml' ? yaml.load(el.innerHTML) : JSON.parse(el.innerHTML)
|
||||
el.innerHTML = ''
|
||||
|
||||
const canvas = document.createElement('canvas')
|
||||
|
||||
@@ -78,8 +78,10 @@ class MarkdownSplitEditor 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
|
||||
@@ -88,10 +90,10 @@ class MarkdownSplitEditor 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'))
|
||||
}
|
||||
@@ -172,6 +174,7 @@ class MarkdownSplitEditor extends React.Component {
|
||||
noteKey={noteKey}
|
||||
onChange={this.handleOnChange.bind(this)}
|
||||
onScroll={this.handleScroll.bind(this)}
|
||||
spellCheck={config.editor.spellcheck}
|
||||
/>
|
||||
<div styleName='slider' style={{left: this.state.codeEditorWidthInPercent + '%'}} onMouseDown={e => this.handleMouseDown(e)} >
|
||||
<div styleName='slider-hitbox' />
|
||||
|
||||
@@ -373,6 +373,36 @@ for name, val in admonition_types
|
||||
color: val[color]
|
||||
content: val[icon]
|
||||
|
||||
dl
|
||||
margin 2rem 0
|
||||
padding 0
|
||||
display flex
|
||||
width 100%
|
||||
flex-wrap wrap
|
||||
align-items flex-start
|
||||
border-bottom 1px solid borderColor
|
||||
background-color tableHeadBgColor
|
||||
|
||||
dt
|
||||
border-top 1px solid borderColor
|
||||
font-weight bold
|
||||
text-align right
|
||||
overflow hidden
|
||||
flex-basis 20%
|
||||
padding 0.4rem 0.9rem
|
||||
box-sizing border-box
|
||||
|
||||
dd
|
||||
border-top 1px solid borderColor
|
||||
flex-basis 80%
|
||||
padding 0.4rem 0.9rem
|
||||
min-height 2.5rem
|
||||
background-color $ui-noteDetail-backgroundColor
|
||||
box-sizing border-box
|
||||
|
||||
dd + dd
|
||||
margin-left 20%
|
||||
|
||||
pre.fence
|
||||
flex-wrap wrap
|
||||
|
||||
@@ -436,6 +466,14 @@ body[data-theme="dark"]
|
||||
kbd
|
||||
background-color themeDarkBorder
|
||||
color themeDarkText
|
||||
dl
|
||||
border-color themeDarkBorder
|
||||
background-color themeDarkTableHead
|
||||
dt
|
||||
border-color themeDarkBorder
|
||||
dd
|
||||
border-color themeDarkBorder
|
||||
background-color themeDarkPreview
|
||||
|
||||
themeSolarizedDarkTableOdd = $ui-solarized-dark-noteDetail-backgroundColor
|
||||
themeSolarizedDarkTableEven = darken($ui-solarized-dark-noteDetail-backgroundColor, 10%)
|
||||
@@ -463,6 +501,14 @@ body[data-theme="solarized-dark"]
|
||||
border-color themeSolarizedDarkTableBorder
|
||||
&:last-child
|
||||
border-right solid 1px themeSolarizedDarkTableBorder
|
||||
dl
|
||||
border-color themeDarkBorder
|
||||
background-color themeSolarizedDarkTableHead
|
||||
dt
|
||||
border-color themeDarkBorder
|
||||
dd
|
||||
border-color themeDarkBorder
|
||||
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
||||
|
||||
themeMonokaiTableOdd = $ui-monokai-noteDetail-backgroundColor
|
||||
themeMonokaiTableEven = darken($ui-monokai-noteDetail-backgroundColor, 10%)
|
||||
@@ -492,6 +538,14 @@ body[data-theme="monokai"]
|
||||
border-right solid 1px themeMonokaiTableBorder
|
||||
kbd
|
||||
background-color themeDarkBackground
|
||||
dl
|
||||
border-color themeDarkBorder
|
||||
background-color themeMonokaiTableHead
|
||||
dt
|
||||
border-color themeDarkBorder
|
||||
dd
|
||||
border-color themeDarkBorder
|
||||
background-color $ui-monokai-noteDetail-backgroundColor
|
||||
|
||||
themeDraculaTableOdd = $ui-dracula-noteDetail-backgroundColor
|
||||
themeDraculaTableEven = darken($ui-dracula-noteDetail-backgroundColor, 10%)
|
||||
@@ -520,4 +574,12 @@ body[data-theme="dracula"]
|
||||
&:last-child
|
||||
border-right solid 1px themeDraculaTableBorder
|
||||
kbd
|
||||
background-color themeDarkBackground
|
||||
background-color themeDarkBackground
|
||||
dl
|
||||
border-color themeDarkBorder
|
||||
background-color themeDraculaTableHead
|
||||
dt
|
||||
border-color themeDarkBorder
|
||||
dd
|
||||
border-color themeDarkBorder
|
||||
background-color $ui-dracula-noteDetail-backgroundColor
|
||||
|
||||
@@ -25,7 +25,7 @@ function render (element, content, theme) {
|
||||
if (height && height.value !== 'undefined') {
|
||||
element.style.height = height.value + 'vh'
|
||||
}
|
||||
let isDarkTheme = theme === 'dark' || theme === 'solarized-dark' || theme === 'monokai' || theme === 'dracula'
|
||||
const isDarkTheme = theme === 'dark' || theme === 'solarized-dark' || theme === 'monokai' || theme === 'dracula'
|
||||
mermaidAPI.initialize({
|
||||
theme: isDarkTheme ? 'dark' : 'default',
|
||||
themeCSS: isDarkTheme ? darkThemeStyling : '',
|
||||
|
||||
@@ -48,9 +48,13 @@ const languages = [
|
||||
locale: 'pl'
|
||||
},
|
||||
{
|
||||
name: 'Portuguese',
|
||||
name: 'Portuguese (PT-BR)',
|
||||
locale: 'pt-BR'
|
||||
},
|
||||
{
|
||||
name: 'Portuguese (PT-PT)',
|
||||
locale: 'pt-PT'
|
||||
},
|
||||
{
|
||||
name: 'Russian',
|
||||
locale: 'ru'
|
||||
|
||||
65
browser/lib/contextMenuBuilder.js
Normal file
65
browser/lib/contextMenuBuilder.js
Normal file
@@ -0,0 +1,65 @@
|
||||
const {remote} = require('electron')
|
||||
const {Menu} = remote.require('electron')
|
||||
const spellcheck = require('./spellcheck')
|
||||
|
||||
/**
|
||||
* Creates the context menu that is shown when there is a right click in the editor of a (not-snippet) note.
|
||||
* If the word is does not contains a spelling error (determined by the 'error style'), no suggestions for corrections are requested
|
||||
* => they are not visible in the context menu
|
||||
* @param editor CodeMirror editor
|
||||
* @param {MouseEvent} event that has triggered the creation of the context menu
|
||||
* @returns {Electron.Menu} The created electron context menu
|
||||
*/
|
||||
const buildEditorContextMenu = function (editor, event) {
|
||||
if (editor == null || event == null || event.pageX == null || event.pageY == null) {
|
||||
return null
|
||||
}
|
||||
const cursor = editor.coordsChar({left: event.pageX, top: event.pageY})
|
||||
const wordRange = editor.findWordAt(cursor)
|
||||
const word = editor.getRange(wordRange.anchor, wordRange.head)
|
||||
const existingMarks = editor.findMarks(wordRange.anchor, wordRange.head) || []
|
||||
let isMisspelled = false
|
||||
for (const mark of existingMarks) {
|
||||
if (mark.className === spellcheck.getCSSClassName()) {
|
||||
isMisspelled = true
|
||||
break
|
||||
}
|
||||
}
|
||||
let suggestion = []
|
||||
if (isMisspelled) {
|
||||
suggestion = spellcheck.getSpellingSuggestion(word)
|
||||
}
|
||||
|
||||
const selection = {
|
||||
isMisspelled: isMisspelled,
|
||||
spellingSuggestions: suggestion
|
||||
}
|
||||
const template = [{
|
||||
role: 'cut'
|
||||
}, {
|
||||
role: 'copy'
|
||||
}, {
|
||||
role: 'paste'
|
||||
}, {
|
||||
role: 'selectall'
|
||||
}]
|
||||
|
||||
if (selection.isMisspelled) {
|
||||
const suggestions = selection.spellingSuggestions
|
||||
template.unshift.apply(template, suggestions.map(function (suggestion) {
|
||||
return {
|
||||
label: suggestion,
|
||||
click: function (suggestion) {
|
||||
if (editor != null) {
|
||||
editor.replaceRange(suggestion.label, wordRange.anchor, wordRange.head)
|
||||
}
|
||||
}
|
||||
}
|
||||
}).concat({
|
||||
type: 'separator'
|
||||
}))
|
||||
}
|
||||
return Menu.buildFromTemplate(template)
|
||||
}
|
||||
|
||||
module.exports = buildEditorContextMenu
|
||||
232
browser/lib/markdown-it-deflist.js
Normal file
232
browser/lib/markdown-it-deflist.js
Normal file
@@ -0,0 +1,232 @@
|
||||
'use strict'
|
||||
|
||||
module.exports = function definitionListPlugin (md) {
|
||||
var isSpace = md.utils.isSpace
|
||||
|
||||
// Search `[:~][\n ]`, returns next pos after marker on success
|
||||
// or -1 on fail.
|
||||
function skipMarker (state, line) {
|
||||
let start = state.bMarks[line] + state.tShift[line]
|
||||
const max = state.eMarks[line]
|
||||
|
||||
if (start >= max) { return -1 }
|
||||
|
||||
// Check bullet
|
||||
const marker = state.src.charCodeAt(start++)
|
||||
if (marker !== 0x7E/* ~ */ && marker !== 0x3A/* : */) { return -1 }
|
||||
|
||||
const pos = state.skipSpaces(start)
|
||||
|
||||
// require space after ":"
|
||||
if (start === pos) { return -1 }
|
||||
|
||||
return start
|
||||
}
|
||||
|
||||
function markTightParagraphs (state, idx) {
|
||||
const level = state.level + 2
|
||||
|
||||
let i
|
||||
let l
|
||||
for (i = idx + 2, l = state.tokens.length - 2; i < l; i++) {
|
||||
if (state.tokens[i].level === level && state.tokens[i].type === 'paragraph_open') {
|
||||
state.tokens[i + 2].hidden = true
|
||||
state.tokens[i].hidden = true
|
||||
i += 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function deflist (state, startLine, endLine, silent) {
|
||||
var ch,
|
||||
contentStart,
|
||||
ddLine,
|
||||
dtLine,
|
||||
itemLines,
|
||||
listLines,
|
||||
listTokIdx,
|
||||
max,
|
||||
newEndLine,
|
||||
nextLine,
|
||||
offset,
|
||||
oldDDIndent,
|
||||
oldIndent,
|
||||
oldLineMax,
|
||||
oldParentType,
|
||||
oldSCount,
|
||||
oldTShift,
|
||||
oldTight,
|
||||
pos,
|
||||
prevEmptyEnd,
|
||||
tight,
|
||||
token
|
||||
|
||||
if (silent) {
|
||||
// quirk: validation mode validates a dd block only, not a whole deflist
|
||||
if (state.ddIndent < 0) { return false }
|
||||
return skipMarker(state, startLine) >= 0
|
||||
}
|
||||
|
||||
nextLine = startLine + 1
|
||||
if (nextLine >= endLine) { return false }
|
||||
|
||||
if (state.isEmpty(nextLine)) {
|
||||
nextLine++
|
||||
if (nextLine >= endLine) { return false }
|
||||
}
|
||||
|
||||
if (state.sCount[nextLine] < state.blkIndent) { return false }
|
||||
contentStart = skipMarker(state, nextLine)
|
||||
if (contentStart < 0) { return false }
|
||||
|
||||
// Start list
|
||||
listTokIdx = state.tokens.length
|
||||
tight = true
|
||||
|
||||
token = state.push('dl_open', 'dl', 1)
|
||||
token.map = listLines = [ startLine, 0 ]
|
||||
|
||||
//
|
||||
// Iterate list items
|
||||
//
|
||||
|
||||
dtLine = startLine
|
||||
ddLine = nextLine
|
||||
|
||||
// One definition list can contain multiple DTs,
|
||||
// and one DT can be followed by multiple DDs.
|
||||
//
|
||||
// Thus, there is two loops here, and label is
|
||||
// needed to break out of the second one
|
||||
//
|
||||
/* eslint no-labels:0,block-scoped-var:0 */
|
||||
OUTER:
|
||||
for (;;) {
|
||||
prevEmptyEnd = false
|
||||
|
||||
token = state.push('dt_open', 'dt', 1)
|
||||
token.map = [ dtLine, dtLine ]
|
||||
|
||||
token = state.push('inline', '', 0)
|
||||
token.map = [ dtLine, dtLine ]
|
||||
token.content = state.getLines(dtLine, dtLine + 1, state.blkIndent, false).trim()
|
||||
token.children = []
|
||||
|
||||
token = state.push('dt_close', 'dt', -1)
|
||||
|
||||
for (;;) {
|
||||
token = state.push('dd_open', 'dd', 1)
|
||||
token.map = itemLines = [ ddLine, 0 ]
|
||||
|
||||
pos = contentStart
|
||||
max = state.eMarks[ddLine]
|
||||
offset = state.sCount[ddLine] + contentStart - (state.bMarks[ddLine] + state.tShift[ddLine])
|
||||
|
||||
while (pos < max) {
|
||||
ch = state.src.charCodeAt(pos)
|
||||
|
||||
if (isSpace(ch)) {
|
||||
if (ch === 0x09) {
|
||||
offset += 4 - offset % 4
|
||||
} else {
|
||||
offset++
|
||||
}
|
||||
} else {
|
||||
break
|
||||
}
|
||||
|
||||
pos++
|
||||
}
|
||||
|
||||
contentStart = pos
|
||||
|
||||
oldTight = state.tight
|
||||
oldDDIndent = state.ddIndent
|
||||
oldIndent = state.blkIndent
|
||||
oldTShift = state.tShift[ddLine]
|
||||
oldSCount = state.sCount[ddLine]
|
||||
oldParentType = state.parentType
|
||||
state.blkIndent = state.ddIndent = state.sCount[ddLine] + 2
|
||||
state.tShift[ddLine] = contentStart - state.bMarks[ddLine]
|
||||
state.sCount[ddLine] = offset
|
||||
state.tight = true
|
||||
state.parentType = 'deflist'
|
||||
|
||||
newEndLine = ddLine
|
||||
while (++newEndLine < endLine && (state.sCount[newEndLine] >= state.sCount[ddLine] || state.isEmpty(newEndLine))) {
|
||||
}
|
||||
|
||||
oldLineMax = state.lineMax
|
||||
state.lineMax = newEndLine
|
||||
|
||||
state.md.block.tokenize(state, ddLine, newEndLine, true)
|
||||
|
||||
state.lineMax = oldLineMax
|
||||
|
||||
// If any of list item is tight, mark list as tight
|
||||
if (!state.tight || prevEmptyEnd) {
|
||||
tight = false
|
||||
}
|
||||
// Item become loose if finish with empty line,
|
||||
// but we should filter last element, because it means list finish
|
||||
prevEmptyEnd = (state.line - ddLine) > 1 && state.isEmpty(state.line - 1)
|
||||
|
||||
state.tShift[ddLine] = oldTShift
|
||||
state.sCount[ddLine] = oldSCount
|
||||
state.tight = oldTight
|
||||
state.parentType = oldParentType
|
||||
state.blkIndent = oldIndent
|
||||
state.ddIndent = oldDDIndent
|
||||
|
||||
token = state.push('dd_close', 'dd', -1)
|
||||
|
||||
itemLines[1] = nextLine = state.line
|
||||
|
||||
if (nextLine >= endLine) { break OUTER }
|
||||
|
||||
if (state.sCount[nextLine] < state.blkIndent) { break OUTER }
|
||||
contentStart = skipMarker(state, nextLine)
|
||||
if (contentStart < 0) { break }
|
||||
|
||||
ddLine = nextLine
|
||||
|
||||
// go to the next loop iteration:
|
||||
// insert DD tag and repeat checking
|
||||
}
|
||||
|
||||
if (nextLine >= endLine) { break }
|
||||
dtLine = nextLine
|
||||
|
||||
if (state.isEmpty(dtLine)) { break }
|
||||
if (state.sCount[dtLine] < state.blkIndent) { break }
|
||||
|
||||
ddLine = dtLine + 1
|
||||
if (ddLine >= endLine) { break }
|
||||
if (state.isEmpty(ddLine)) { ddLine++ }
|
||||
if (ddLine >= endLine) { break }
|
||||
|
||||
if (state.sCount[ddLine] < state.blkIndent) { break }
|
||||
contentStart = skipMarker(state, ddLine)
|
||||
if (contentStart < 0) { break }
|
||||
|
||||
// go to the next loop iteration:
|
||||
// insert DT and DD tags and repeat checking
|
||||
}
|
||||
|
||||
// Finilize list
|
||||
token = state.push('dl_close', 'dl', -1)
|
||||
|
||||
listLines[1] = nextLine
|
||||
|
||||
state.line = nextLine
|
||||
|
||||
// mark paragraphs tight if needed
|
||||
if (tight) {
|
||||
markTightParagraphs(state, listTokIdx)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
md.block.ruler.before('paragraph', 'deflist', deflist, { alt: [ 'paragraph', 'reference' ] })
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
'use strict'
|
||||
|
||||
module.exports = function (md, renderers, defaultRenderer) {
|
||||
function fence (state, startLine, endLine) {
|
||||
const paramsRE = /^[ \t]*([\w+#-]+)?(?:\(((?:\s*\w[-\w]*(?:=(?:'(?:.*?[^\\])?'|"(?:.*?[^\\])?"|(?:[^'"][^\s]*)))?)*)\))?(?::([^:]*)(?::(\d+))?)?\s*$/
|
||||
|
||||
function fence (state, startLine, endLine, silent) {
|
||||
let pos = state.bMarks[startLine] + state.tShift[startLine]
|
||||
let max = state.eMarks[startLine]
|
||||
|
||||
@@ -10,7 +12,7 @@ module.exports = function (md, renderers, defaultRenderer) {
|
||||
}
|
||||
|
||||
const marker = state.src.charCodeAt(pos)
|
||||
if (!(marker === 96 || marker === 126)) {
|
||||
if (marker !== 0x7E/* ~ */ && marker !== 0x60 /* ` */) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -25,6 +27,10 @@ module.exports = function (md, renderers, defaultRenderer) {
|
||||
const markup = state.src.slice(mem, pos)
|
||||
const params = state.src.slice(pos, max)
|
||||
|
||||
if (silent) {
|
||||
return true
|
||||
}
|
||||
|
||||
let nextLine = startLine
|
||||
let haveEndMarker = false
|
||||
|
||||
@@ -66,7 +72,7 @@ module.exports = function (md, renderers, defaultRenderer) {
|
||||
let fileName = ''
|
||||
let firstLineNumber = 1
|
||||
|
||||
let match = /^(\w[-\w]*)?(?:\(((?:\s*\w[-\w]*(?:=(?:'(?:.*?[^\\])?'|"(?:.*?[^\\])?"|(?:[^'"][^\s]*)))?)*)\))?(?::([^:]*)(?::(\d+))?)?\s*$/.exec(params)
|
||||
let match = paramsRE.exec(params)
|
||||
if (match) {
|
||||
if (match[1]) {
|
||||
langType = match[1]
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import toc from 'markdown-toc'
|
||||
import diacritics from 'diacritics-map'
|
||||
import stripColor from 'strip-color'
|
||||
import mdlink from 'markdown-link'
|
||||
|
||||
const EOL = require('os').EOL
|
||||
|
||||
@@ -42,6 +43,12 @@ function caseSensitiveSlugify (str) {
|
||||
return str
|
||||
}
|
||||
|
||||
function linkify (tok, text, slug, opts) {
|
||||
var uniqeID = opts.num === 0 ? '' : '-' + opts.num
|
||||
tok.content = mdlink(text, '#' + slug + uniqeID)
|
||||
return tok
|
||||
}
|
||||
|
||||
const TOC_MARKER_START = '<!-- toc -->'
|
||||
const TOC_MARKER_END = '<!-- tocstop -->'
|
||||
|
||||
@@ -84,7 +91,7 @@ export function generateInEditor (editor) {
|
||||
* @returns generatedTOC String containing generated TOC
|
||||
*/
|
||||
export function generate (markdownText) {
|
||||
const generatedToc = toc(markdownText, {slugify: caseSensitiveSlugify})
|
||||
const generatedToc = toc(markdownText, {slugify: caseSensitiveSlugify, linkify: linkify})
|
||||
return TOC_MARKER_START + EOL + EOL + generatedToc.content + EOL + EOL + TOC_MARKER_END
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import _ from 'lodash'
|
||||
import ConfigManager from 'browser/main/lib/ConfigManager'
|
||||
import katex from 'katex'
|
||||
import { lastFindInArray } from './utils'
|
||||
import ee from 'browser/main/lib/eventEmitter'
|
||||
import anchor from '@enyaxu/markdown-it-anchor'
|
||||
|
||||
function createGutter (str, firstLineNumber) {
|
||||
if (Number.isNaN(firstLineNumber)) firstLineNumber = 1
|
||||
@@ -118,24 +118,32 @@ 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 `<pre class="fence" data-line="${token.map[0]}">
|
||||
<span class="filename">${token.fileName}</span>
|
||||
<div class="chart" data-height="${token.parameters.height}">${token.content}</div>
|
||||
<div class="chart" data-height="${token.parameters.height}" data-format="${token.parameters.format || 'json'}">${token.content}</div>
|
||||
</pre>`
|
||||
},
|
||||
flowchart: token => {
|
||||
@@ -264,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]])
|
||||
}
|
||||
|
||||
@@ -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: [
|
||||
{
|
||||
|
||||
232
browser/lib/spellcheck.js
Normal file
232
browser/lib/spellcheck.js
Normal file
@@ -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: <code>{anchor: {line: integer, ch: integer}, head: {line: integer, ch: integer}}</code>
|
||||
*/
|
||||
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
|
||||
}
|
||||
@@ -190,6 +190,36 @@ class MarkdownNoteDetail extends React.Component {
|
||||
ee.emit('export:save-html')
|
||||
}
|
||||
|
||||
handleKeyDown (e) {
|
||||
switch (e.keyCode) {
|
||||
// tab key
|
||||
case 9:
|
||||
if (e.ctrlKey && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
this.jumpNextTab()
|
||||
} else if (e.ctrlKey && e.shiftKey) {
|
||||
e.preventDefault()
|
||||
this.jumpPrevTab()
|
||||
} else if (!e.ctrlKey && !e.shiftKey && e.target === this.refs.description) {
|
||||
e.preventDefault()
|
||||
this.focusEditor()
|
||||
}
|
||||
break
|
||||
// I key
|
||||
case 73:
|
||||
{
|
||||
const isSuper = global.process.platform === 'darwin'
|
||||
? e.metaKey
|
||||
: e.ctrlKey
|
||||
if (isSuper) {
|
||||
e.preventDefault()
|
||||
this.handleInfoButtonClick(e)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
handleTrashButtonClick (e) {
|
||||
const { note } = this.state
|
||||
const { isTrashed } = note
|
||||
@@ -458,6 +488,7 @@ class MarkdownNoteDetail extends React.Component {
|
||||
<div className='NoteDetail'
|
||||
style={this.props.style}
|
||||
styleName='root'
|
||||
onKeyDown={(e) => this.handleKeyDown(e)}
|
||||
>
|
||||
|
||||
{location.pathname === '/trashed' ? trashTopBar : detailTopBar}
|
||||
|
||||
@@ -434,6 +434,18 @@ class SnippetNoteDetail extends React.Component {
|
||||
this.focusEditor()
|
||||
}
|
||||
break
|
||||
// I key
|
||||
case 73:
|
||||
{
|
||||
const isSuper = global.process.platform === 'darwin'
|
||||
? e.metaKey
|
||||
: e.ctrlKey
|
||||
if (isSuper) {
|
||||
e.preventDefault()
|
||||
this.handleInfoButtonClick(e)
|
||||
}
|
||||
}
|
||||
break
|
||||
// L key
|
||||
case 76:
|
||||
{
|
||||
|
||||
@@ -167,6 +167,8 @@ class Main extends React.Component {
|
||||
}
|
||||
})
|
||||
|
||||
delete CodeMirror.keyMap.emacs['Ctrl-V']
|
||||
|
||||
eventEmitter.on('editor:fullscreen', this.toggleFullScreen)
|
||||
}
|
||||
|
||||
|
||||
@@ -35,19 +35,20 @@ class NewNoteButton extends React.Component {
|
||||
}
|
||||
|
||||
handleNewNoteButtonClick (e) {
|
||||
const { location, dispatch, config } = this.props
|
||||
const { location, params, dispatch, config } = this.props
|
||||
const { storage, folder } = this.resolveTargetFolder()
|
||||
|
||||
if (config.ui.defaultNote === 'MARKDOWN_NOTE') {
|
||||
createMarkdownNote(storage.key, folder.key, dispatch, location)
|
||||
createMarkdownNote(storage.key, folder.key, dispatch, location, params, config)
|
||||
} else if (config.ui.defaultNote === 'SNIPPET_NOTE') {
|
||||
createSnippetNote(storage.key, folder.key, dispatch, location, config)
|
||||
createSnippetNote(storage.key, folder.key, dispatch, location, params, config)
|
||||
} else {
|
||||
modal.open(NewNoteModal, {
|
||||
storage: storage.key,
|
||||
folder: folder.key,
|
||||
dispatch,
|
||||
location,
|
||||
params,
|
||||
config
|
||||
})
|
||||
}
|
||||
|
||||
@@ -83,7 +83,9 @@ class NoteList extends React.Component {
|
||||
|
||||
// TODO: not Selected noteKeys but SelectedNote(for reusing)
|
||||
this.state = {
|
||||
ctrlKeyDown: false,
|
||||
shiftKeyDown: false,
|
||||
prevShiftNoteIndex: -1,
|
||||
selectedNoteKeys: []
|
||||
}
|
||||
|
||||
@@ -171,16 +173,15 @@ class NoteList extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
focusNote (selectedNoteKeys, noteKey) {
|
||||
focusNote (selectedNoteKeys, noteKey, pathname) {
|
||||
const { router } = this.context
|
||||
const { location } = this.props
|
||||
|
||||
this.setState({
|
||||
selectedNoteKeys
|
||||
})
|
||||
|
||||
router.push({
|
||||
pathname: location.pathname,
|
||||
pathname,
|
||||
query: {
|
||||
key: noteKey
|
||||
}
|
||||
@@ -199,6 +200,7 @@ class NoteList extends React.Component {
|
||||
}
|
||||
let { selectedNoteKeys } = this.state
|
||||
const { shiftKeyDown } = this.state
|
||||
const { location } = this.props
|
||||
|
||||
let targetIndex = this.getTargetIndex()
|
||||
|
||||
@@ -215,7 +217,7 @@ class NoteList extends React.Component {
|
||||
selectedNoteKeys.push(priorNoteKey)
|
||||
}
|
||||
|
||||
this.focusNote(selectedNoteKeys, priorNoteKey)
|
||||
this.focusNote(selectedNoteKeys, priorNoteKey, location.pathname)
|
||||
|
||||
ee.emit('list:moved')
|
||||
}
|
||||
@@ -226,6 +228,7 @@ class NoteList extends React.Component {
|
||||
}
|
||||
let { selectedNoteKeys } = this.state
|
||||
const { shiftKeyDown } = this.state
|
||||
const { location } = this.props
|
||||
|
||||
let targetIndex = this.getTargetIndex()
|
||||
const isTargetLastNote = targetIndex === this.notes.length - 1
|
||||
@@ -248,7 +251,7 @@ class NoteList extends React.Component {
|
||||
selectedNoteKeys.push(nextNoteKey)
|
||||
}
|
||||
|
||||
this.focusNote(selectedNoteKeys, nextNoteKey)
|
||||
this.focusNote(selectedNoteKeys, nextNoteKey, location.pathname)
|
||||
|
||||
ee.emit('list:moved')
|
||||
}
|
||||
@@ -260,13 +263,13 @@ class NoteList extends React.Component {
|
||||
}
|
||||
|
||||
const selectedNoteKeys = [noteHash]
|
||||
this.focusNote(selectedNoteKeys, noteHash)
|
||||
this.focusNote(selectedNoteKeys, noteHash, '/home')
|
||||
|
||||
ee.emit('list:moved')
|
||||
}
|
||||
|
||||
handleNoteListKeyDown (e) {
|
||||
if (e.metaKey || e.ctrlKey) return true
|
||||
if (e.metaKey) return true
|
||||
|
||||
// A key
|
||||
if (e.keyCode === 65 && !e.shiftKey) {
|
||||
@@ -306,6 +309,8 @@ class NoteList extends React.Component {
|
||||
|
||||
if (e.shiftKey) {
|
||||
this.setState({ shiftKeyDown: true })
|
||||
} else if (e.ctrlKey) {
|
||||
this.setState({ ctrlKeyDown: true })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -313,6 +318,10 @@ class NoteList extends React.Component {
|
||||
if (!e.shiftKey) {
|
||||
this.setState({ shiftKeyDown: false })
|
||||
}
|
||||
|
||||
if (!e.ctrlKey) {
|
||||
this.setState({ ctrlKeyDown: false })
|
||||
}
|
||||
}
|
||||
|
||||
getNotes () {
|
||||
@@ -389,25 +398,65 @@ class NoteList extends React.Component {
|
||||
return pinnedNotes.concat(unpinnedNotes)
|
||||
}
|
||||
|
||||
getNoteIndexByKey (noteKey) {
|
||||
return this.notes.findIndex((note) => {
|
||||
if (!note) return -1
|
||||
|
||||
return note.key === noteKey
|
||||
})
|
||||
}
|
||||
|
||||
handleNoteClick (e, uniqueKey) {
|
||||
const { router } = this.context
|
||||
const { location } = this.props
|
||||
let { selectedNoteKeys } = this.state
|
||||
const { shiftKeyDown } = this.state
|
||||
let { selectedNoteKeys, prevShiftNoteIndex } = this.state
|
||||
const { ctrlKeyDown, shiftKeyDown } = this.state
|
||||
const hasSelectedNoteKey = selectedNoteKeys.length > 0
|
||||
|
||||
if (shiftKeyDown && selectedNoteKeys.includes(uniqueKey)) {
|
||||
if (ctrlKeyDown && selectedNoteKeys.includes(uniqueKey)) {
|
||||
const newSelectedNoteKeys = selectedNoteKeys.filter((noteKey) => noteKey !== uniqueKey)
|
||||
this.setState({
|
||||
selectedNoteKeys: newSelectedNoteKeys
|
||||
})
|
||||
return
|
||||
}
|
||||
if (!shiftKeyDown) {
|
||||
if (!ctrlKeyDown && !shiftKeyDown) {
|
||||
selectedNoteKeys = []
|
||||
}
|
||||
|
||||
if (!shiftKeyDown) {
|
||||
prevShiftNoteIndex = -1
|
||||
}
|
||||
|
||||
selectedNoteKeys.push(uniqueKey)
|
||||
|
||||
if (shiftKeyDown && hasSelectedNoteKey) {
|
||||
let firstShiftNoteIndex = this.getNoteIndexByKey(selectedNoteKeys[0])
|
||||
// Shift selection can either start from first note in the exisiting selectedNoteKeys
|
||||
// or previous first shift note index
|
||||
firstShiftNoteIndex = firstShiftNoteIndex > prevShiftNoteIndex
|
||||
? firstShiftNoteIndex : prevShiftNoteIndex
|
||||
|
||||
const lastShiftNoteIndex = this.getNoteIndexByKey(uniqueKey)
|
||||
|
||||
const startIndex = firstShiftNoteIndex < lastShiftNoteIndex
|
||||
? firstShiftNoteIndex : lastShiftNoteIndex
|
||||
const endIndex = firstShiftNoteIndex > lastShiftNoteIndex
|
||||
? firstShiftNoteIndex : lastShiftNoteIndex
|
||||
|
||||
selectedNoteKeys = []
|
||||
for (let i = startIndex; i <= endIndex; i++) {
|
||||
selectedNoteKeys.push(this.notes[i].key)
|
||||
}
|
||||
|
||||
if (prevShiftNoteIndex < 0) {
|
||||
prevShiftNoteIndex = firstShiftNoteIndex
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
selectedNoteKeys
|
||||
selectedNoteKeys,
|
||||
prevShiftNoteIndex
|
||||
})
|
||||
|
||||
router.push({
|
||||
|
||||
@@ -274,7 +274,7 @@ class StorageItem extends React.Component {
|
||||
const { folderNoteMap, trashedSet } = data
|
||||
const SortableStorageItemChild = SortableElement(StorageItemChild)
|
||||
const folderList = storage.folders.map((folder, index) => {
|
||||
let folderRegex = new RegExp(escapeStringRegexp(path.sep) + 'storages' + escapeStringRegexp(path.sep) + storage.key + escapeStringRegexp(path.sep) + 'folders' + escapeStringRegexp(path.sep) + folder.key)
|
||||
const folderRegex = new RegExp(escapeStringRegexp(path.sep) + 'storages' + escapeStringRegexp(path.sep) + storage.key + escapeStringRegexp(path.sep) + 'folders' + escapeStringRegexp(path.sep) + folder.key)
|
||||
const isActive = !!(location.pathname.match(folderRegex))
|
||||
const noteSet = folderNoteMap.get(storage.key + '-' + folder.key)
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import {SortableContainer} from 'react-sortable-hoc'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
import context from 'browser/lib/context'
|
||||
import { remote } from 'electron'
|
||||
import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
|
||||
|
||||
function matchActiveTags (tags, activeTags) {
|
||||
return _.every(activeTags, v => tags.indexOf(v) >= 0)
|
||||
@@ -269,7 +270,7 @@ class SideNav extends React.Component {
|
||||
const tags = pathSegments[pathSegments.length - 1]
|
||||
return (tags === 'alltags')
|
||||
? []
|
||||
: tags.split(' ').map(tag => decodeURIComponent(tag))
|
||||
: decodeURIComponent(tags).split(' ')
|
||||
}
|
||||
|
||||
handleClickTagListItem (name) {
|
||||
@@ -301,7 +302,7 @@ class SideNav extends React.Component {
|
||||
} else {
|
||||
listOfTags.push(tag)
|
||||
}
|
||||
router.push(`/tags/${listOfTags.map(tag => encodeURIComponent(tag)).join(' ')}`)
|
||||
router.push(`/tags/${encodeURIComponent(listOfTags.join(' '))}`)
|
||||
}
|
||||
|
||||
emptyTrash (entries) {
|
||||
@@ -309,6 +310,8 @@ class SideNav extends React.Component {
|
||||
const deletionPromises = entries.map((note) => {
|
||||
return dataApi.deleteNote(note.storage, note.key)
|
||||
})
|
||||
const { confirmDeletion } = this.props.config.ui
|
||||
if (!confirmDeleteNote(confirmDeletion, true)) return
|
||||
Promise.all(deletionPromises)
|
||||
.then((arrayOfStorageAndNoteKeys) => {
|
||||
arrayOfStorageAndNoteKeys.forEach(({ storageKey, noteKey }) => {
|
||||
|
||||
@@ -47,6 +47,14 @@
|
||||
.update-icon
|
||||
color $brand-color
|
||||
|
||||
body[data-theme="default"]
|
||||
.zoom
|
||||
color $ui-text-color
|
||||
|
||||
body[data-theme="white"]
|
||||
.zoom
|
||||
color $ui-text-color
|
||||
|
||||
body[data-theme="dark"]
|
||||
.root
|
||||
border-color $ui-dark-borderColor
|
||||
|
||||
@@ -5,6 +5,7 @@ import styles from './StatusBar.styl'
|
||||
import ZoomManager from 'browser/main/lib/ZoomManager'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
import context from 'browser/lib/context'
|
||||
import EventEmitter from 'browser/main/lib/eventEmitter'
|
||||
|
||||
const electron = require('electron')
|
||||
const { remote, ipcRenderer } = electron
|
||||
@@ -13,6 +14,26 @@ const { dialog } = remote
|
||||
const zoomOptions = [0.8, 0.9, 1, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2.0]
|
||||
|
||||
class StatusBar extends React.Component {
|
||||
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.handleZoomInMenuItem = this.handleZoomInMenuItem.bind(this)
|
||||
this.handleZoomOutMenuItem = this.handleZoomOutMenuItem.bind(this)
|
||||
this.handleZoomResetMenuItem = this.handleZoomResetMenuItem.bind(this)
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
EventEmitter.on('status:zoomin', this.handleZoomInMenuItem)
|
||||
EventEmitter.on('status:zoomout', this.handleZoomOutMenuItem)
|
||||
EventEmitter.on('status:zoomreset', this.handleZoomResetMenuItem)
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
EventEmitter.off('status:zoomin', this.handleZoomInMenuItem)
|
||||
EventEmitter.off('status:zoomout', this.handleZoomOutMenuItem)
|
||||
EventEmitter.off('status:zoomreset', this.handleZoomResetMenuItem)
|
||||
}
|
||||
|
||||
updateApp () {
|
||||
const index = dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
type: 'warning',
|
||||
@@ -48,6 +69,20 @@ class StatusBar extends React.Component {
|
||||
})
|
||||
}
|
||||
|
||||
handleZoomInMenuItem () {
|
||||
const zoomFactor = ZoomManager.getZoom() + 0.1
|
||||
this.handleZoomMenuItemClick(zoomFactor)
|
||||
}
|
||||
|
||||
handleZoomOutMenuItem () {
|
||||
const zoomFactor = ZoomManager.getZoom() - 0.1
|
||||
this.handleZoomMenuItemClick(zoomFactor)
|
||||
}
|
||||
|
||||
handleZoomResetMenuItem () {
|
||||
this.handleZoomMenuItemClick(1.0)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { config, status } = this.context
|
||||
|
||||
|
||||
@@ -54,7 +54,8 @@ export const DEFAULT_CONFIG = {
|
||||
fetchUrlTitle: true,
|
||||
enableTableEditor: false,
|
||||
enableFrontMatterTitle: true,
|
||||
frontMatterTitleField: 'title'
|
||||
frontMatterTitleField: 'title',
|
||||
spellcheck: false
|
||||
},
|
||||
preview: {
|
||||
fontSize: '14',
|
||||
@@ -206,7 +207,7 @@ function rewriteHotkey (config) {
|
||||
const keys = [...Object.keys(config.hotkey)]
|
||||
keys.forEach(key => {
|
||||
config.hotkey[key] = config.hotkey[key].replace(/Cmd/g, 'Command')
|
||||
config.hotkey[key] = config.hotkey[key].replace(/Opt/g, 'Alt')
|
||||
config.hotkey[key] = config.hotkey[key].replace(/Opt\s/g, 'Option ')
|
||||
})
|
||||
return config
|
||||
}
|
||||
|
||||
@@ -21,8 +21,8 @@ class NewNoteModal extends React.Component {
|
||||
}
|
||||
|
||||
handleMarkdownNoteButtonClick (e) {
|
||||
const { storage, folder, dispatch, location } = this.props
|
||||
createMarkdownNote(storage, folder, dispatch, location).then(() => {
|
||||
const { storage, folder, dispatch, location, params, config } = this.props
|
||||
createMarkdownNote(storage, folder, dispatch, location, params, config).then(() => {
|
||||
setTimeout(this.props.close, 200)
|
||||
})
|
||||
}
|
||||
@@ -35,8 +35,8 @@ class NewNoteModal extends React.Component {
|
||||
}
|
||||
|
||||
handleSnippetNoteButtonClick (e) {
|
||||
const { storage, folder, dispatch, location, config } = this.props
|
||||
createSnippetNote(storage, folder, dispatch, location, config).then(() => {
|
||||
const { storage, folder, dispatch, location, params, config } = this.props
|
||||
createSnippetNote(storage, folder, dispatch, location, params, config).then(() => {
|
||||
setTimeout(this.props.close, 200)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -68,6 +68,7 @@ class UiTab extends React.Component {
|
||||
theme: this.refs.uiTheme.value,
|
||||
language: this.refs.uiLanguage.value,
|
||||
defaultNote: this.refs.defaultNote.value,
|
||||
tagNewNoteWithFilteringTags: this.refs.tagNewNoteWithFilteringTags.checked,
|
||||
showCopyNotification: this.refs.showCopyNotification.checked,
|
||||
confirmDeletion: this.refs.confirmDeletion.checked,
|
||||
showOnlyRelatedTags: this.refs.showOnlyRelatedTags.checked,
|
||||
@@ -97,7 +98,8 @@ class UiTab extends React.Component {
|
||||
frontMatterTitleField: this.refs.frontMatterTitleField.value,
|
||||
matchingPairs: this.refs.matchingPairs.value,
|
||||
matchingTriples: this.refs.matchingTriples.value,
|
||||
explodingPairs: this.refs.explodingPairs.value
|
||||
explodingPairs: this.refs.explodingPairs.value,
|
||||
spellcheck: this.refs.spellcheck.checked
|
||||
},
|
||||
preview: {
|
||||
fontSize: this.refs.previewFontSize.value,
|
||||
@@ -270,6 +272,7 @@ class UiTab extends React.Component {
|
||||
</div>
|
||||
: null
|
||||
}
|
||||
|
||||
<div styleName='group-header2'>Tags</div>
|
||||
|
||||
<div styleName='group-checkBoxSection'>
|
||||
@@ -316,6 +319,17 @@ class UiTab extends React.Component {
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div styleName='group-checkBoxSection'>
|
||||
<label>
|
||||
<input onChange={(e) => this.handleUIChange(e)}
|
||||
checked={this.state.config.ui.tagNewNoteWithFilteringTags}
|
||||
ref='tagNewNoteWithFilteringTags'
|
||||
type='checkbox'
|
||||
/>
|
||||
{i18n.__('New notes are tagged with the filtering tags')}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div styleName='group-header2'>Editor</div>
|
||||
|
||||
<div styleName='group-section'>
|
||||
@@ -541,6 +555,16 @@ class UiTab extends React.Component {
|
||||
{i18n.__('Enable smart table editor')}
|
||||
</label>
|
||||
</div>
|
||||
<div styleName='group-checkBoxSection'>
|
||||
<label>
|
||||
<input onChange={(e) => this.handleUIChange(e)}
|
||||
checked={this.state.config.editor.spellcheck}
|
||||
ref='spellcheck'
|
||||
type='checkbox'
|
||||
/>
|
||||
{i18n.__('Enable spellcheck - Experimental feature!! :)')}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label'>
|
||||
|
||||
Reference in New Issue
Block a user