diff --git a/browser/components/CodeEditor.js b/browser/components/CodeEditor.js index c36a50c1..a1c74c73 100644 --- a/browser/components/CodeEditor.js +++ b/browser/components/CodeEditor.js @@ -14,6 +14,8 @@ import consts from 'browser/lib/consts' import fs from 'fs' const { ipcRenderer } = require('electron') import normalizeEditorFontFamily from 'browser/lib/normalizeEditorFontFamily' +import TurndownService from 'turndown' +import { gfm } from 'turndown-plugin-gfm' CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js' @@ -57,9 +59,12 @@ export default class CodeEditor extends React.Component { } this.searchHandler = (e, msg) => this.handleSearch(msg) this.searchState = null + this.scrollToLineHandeler = this.scrollToLine.bind(this) this.formatTable = () => this.handleFormatTable() this.editorActivityHandler = () => this.handleEditorActivity() + + this.turndownService = new TurndownService() } handleSearch (msg) { @@ -125,6 +130,7 @@ export default class CodeEditor extends React.Component { componentDidMount () { const { rulers, enableRulers } = this.props const expandSnippet = this.expandSnippet.bind(this) + eventEmitter.on('line:jump', this.scrollToLineHandeler) const defaultSnippet = [ { @@ -475,7 +481,13 @@ export default class CodeEditor extends React.Component { moveCursorTo (row, col) {} - scrollToLine (num) {} + scrollToLine (event, num) { + const cursor = { + line: num, + ch: 1 + } + this.editor.setCursor(cursor) + } focus () { this.editor.focus() @@ -538,7 +550,11 @@ export default class CodeEditor extends React.Component { ) return prevChar === '](' && nextChar === ')' } - if (dataTransferItem.type.match('image')) { + + const pastedHtml = clipboardData.getData('text/html') + if (pastedHtml !== '') { + this.handlePasteHtml(e, editor, pastedHtml) + } else if (dataTransferItem.type.match('image')) { attachmentManagement.handlePastImageEvent( this, storageKey, @@ -608,6 +624,12 @@ export default class CodeEditor extends React.Component { }) } + handlePasteHtml (e, editor, pastedHtml) { + e.preventDefault() + const markdown = this.turndownService.turndown(pastedHtml) + editor.replaceSelection(markdown) + } + mapNormalResponse (response, pastedTxt) { return this.decodeResponse(response).then(body => { return new Promise((resolve, reject) => { diff --git a/browser/components/MarkdownEditor.js b/browser/components/MarkdownEditor.js index 70df16a0..9c8a06d6 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) }) } } @@ -300,6 +307,7 @@ class MarkdownEditor extends React.Component { noteKey={noteKey} customCSS={config.preview.customCSS} allowCustomCSS={config.preview.allowCustomCSS} + lineThroughCheckbox={config.preview.lineThroughCheckbox} /> ) diff --git a/browser/components/MarkdownPreview.js b/browser/components/MarkdownPreview.js index 7e710cee..cc5959c6 100755 --- a/browser/components/MarkdownPreview.js +++ b/browser/components/MarkdownPreview.js @@ -84,7 +84,6 @@ function buildStyle ( url('${appPath}/resources/fonts/MaterialIcons-Regular.woff') format('woff'), url('${appPath}/resources/fonts/MaterialIcons-Regular.ttf') format('truetype'); } -${allowCustomCSS ? customCSS : ''} ${markdownStyle} body { @@ -92,6 +91,11 @@ body { font-size: ${fontSize}px; ${scrollPastEnd && 'padding-bottom: 90vh;'} } +@media print { + body { + padding-bottom: initial; + } +} code { font-family: '${codeBlockFontFamily.join("','")}'; background-color: rgba(0,0,0,0.04); @@ -148,6 +152,8 @@ body p { display: none } } + +${allowCustomCSS ? customCSS : ''} ` } @@ -329,9 +335,7 @@ export default class MarkdownPreview extends React.Component { allowCustomCSS, customCSS ) - let body = this.markdown.render( - escapeHtmlCharacters(noteContent, { detectCodeBlock: true }) - ) + let body = this.markdown.render(noteContent) const files = [this.GetCodeThemeLink(codeBlockTheme), ...CSS_FILES] const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent( noteContent, @@ -488,10 +492,6 @@ export default class MarkdownPreview extends React.Component { eventEmitter.on('export:save-md', this.saveAsMdHandler) eventEmitter.on('export:save-html', this.saveAsHtmlHandler) eventEmitter.on('print', this.printHandler) - eventEmitter.on('config-renew', () => { - this.markdown.updateConfig() - this.rewriteIframe() - }) } componentWillUnmount () { @@ -535,7 +535,8 @@ export default class MarkdownPreview extends React.Component { prevProps.smartQuotes !== this.props.smartQuotes || prevProps.sanitize !== this.props.sanitize || prevProps.smartArrows !== this.props.smartArrows || - prevProps.breaks !== this.props.breaks + prevProps.breaks !== this.props.breaks || + prevProps.lineThroughCheckbox !== this.props.lineThroughCheckbox ) { this.initMarkdown() this.rewriteIframe() @@ -897,6 +898,15 @@ export default class MarkdownPreview extends React.Component { return } + const regexIsLine = /^:line:[0-9]/ + if (regexIsLine.test(linkHash)) { + const numberPattern = /\d+/g + + const lineNumber = parseInt(linkHash.match(numberPattern)[0]) + eventEmitter.emit('line:jump', lineNumber) + return + } + // this will match the old link format storage.key-note.key // e.g. // 877f99c3268608328037-1c211eb7dcb463de6490 diff --git a/browser/components/MarkdownSplitEditor.js b/browser/components/MarkdownSplitEditor.js index d714125a..ca2d3108 100644 --- a/browser/components/MarkdownSplitEditor.js +++ b/browser/components/MarkdownSplitEditor.js @@ -20,12 +20,18 @@ class MarkdownSplitEditor extends React.Component { } } + setValue (value) { + this.refs.code.setValue(value) + } + handleOnChange () { this.value = this.refs.code.value this.props.onChange() } handleScroll (e) { + if (!this.props.config.preview.scrollSync) return + const previewDoc = _.get(this, 'refs.preview.refs.root.contentWindow.document') const codeDoc = _.get(this, 'refs.code.editor.doc') let srcTop, srcHeight, targetTop, targetHeight @@ -192,6 +198,7 @@ class MarkdownSplitEditor extends React.Component { noteKey={noteKey} customCSS={config.preview.customCSS} allowCustomCSS={config.preview.allowCustomCSS} + lineThroughCheckbox={config.preview.lineThroughCheckbox} /> ) diff --git a/browser/components/NoteItem.js b/browser/components/NoteItem.js index 600b7e2d..2fc70a39 100644 --- a/browser/components/NoteItem.js +++ b/browser/components/NoteItem.js @@ -24,16 +24,19 @@ const TagElement = ({ tagName }) => ( /** * @description Tag element list component. * @param {Array|null} tags + * @param {boolean} showTagsAlphabetically * @return {React.Component} */ -const TagElementList = tags => { +const TagElementList = (tags, showTagsAlphabetically) => { if (!isArray(tags)) { return [] } - const tagElements = tags.map(tag => TagElement({ tagName: tag })) - - return tagElements + if (showTagsAlphabetically) { + return _.sortBy(tags).map(tag => TagElement({ tagName: tag })) + } else { + return tags.map(tag => TagElement({ tagName: tag })) + } } /** @@ -55,7 +58,8 @@ const NoteItem = ({ pathname, storageName, folderName, - viewType + viewType, + showTagsAlphabetically }) => (
{note.tags.length > 0 - ? TagElementList(note.tags) + ? TagElementList(note.tags, showTagsAlphabetically) : (
{storageList.length > 0 ? storageList : ( -
No storage mount.
+
No storage mount.
)}
) StorageList.propTypes = { - storgaeList: PropTypes.arrayOf(PropTypes.element).isRequired + storageList: PropTypes.arrayOf(PropTypes.element).isRequired } export default CSSModules(StorageList, styles) diff --git a/browser/components/TagListItem.js b/browser/components/TagListItem.js index 6cd50c9c..eec8ab14 100644 --- a/browser/components/TagListItem.js +++ b/browser/components/TagListItem.js @@ -14,8 +14,8 @@ import CSSModules from 'browser/lib/CSSModules' * @param {bool} isRelated */ -const TagListItem = ({name, handleClickTagListItem, handleClickNarrowToTag, isActive, isRelated, count}) => ( -
+const TagListItem = ({name, handleClickTagListItem, handleClickNarrowToTag, handleContextMenu, isActive, isRelated, count}) => ( +
handleContextMenu(e, name)}> {isRelated ?
diff --git a/browser/components/TodoListPercentage.js b/browser/components/TodoListPercentage.js index 3565f274..b917bbc1 100644 --- a/browser/components/TodoListPercentage.js +++ b/browser/components/TodoListPercentage.js @@ -12,7 +12,7 @@ import styles from './TodoListPercentage.styl' */ const TodoListPercentage = ({ - percentageOfTodo + percentageOfTodo, onClearCheckboxClick }) => (
@@ -20,11 +20,15 @@ const TodoListPercentage = ({

{percentageOfTodo}%

+
+

onClearCheckboxClick(e)}>clear

+
) TodoListPercentage.propTypes = { - percentageOfTodo: PropTypes.number.isRequired + percentageOfTodo: PropTypes.number.isRequired, + onClearCheckboxClick: PropTypes.func.isRequired } export default CSSModules(TodoListPercentage, styles) diff --git a/browser/components/TodoListPercentage.styl b/browser/components/TodoListPercentage.styl index 7b6a7d61..5a0f3257 100644 --- a/browser/components/TodoListPercentage.styl +++ b/browser/components/TodoListPercentage.styl @@ -1,4 +1,5 @@ .percentageBar + display: flex position absolute top 72px right 0px @@ -30,6 +31,20 @@ color #f4f4f4 font-weight 600 +.todoClear + display flex + justify-content: flex-end + position absolute + z-index 120 + width 100% + height 100% + padding 2px 10px + +.todoClearText + color #f4f4f4 + cursor pointer + font-weight 500 + body[data-theme="dark"] .percentageBar background-color #444444 @@ -39,6 +54,9 @@ body[data-theme="dark"] .percentageText color $ui-dark-text-color + + .todoClearText + color $ui-dark-text-color body[data-theme="solarized-dark"] .percentageBar @@ -50,6 +68,9 @@ body[data-theme="solarized-dark"] .percentageText color #fdf6e3 + .todoClearText + color #fdf6e3 + body[data-theme="monokai"] .percentageBar background-color: $ui-monokai-borderColor @@ -69,3 +90,6 @@ body[data-theme="dracula"] .percentageText color $ui-dracula-text-color + + .percentageText + color $ui-dracula-text-color diff --git a/browser/components/markdown.styl b/browser/components/markdown.styl index 6272fad2..4d03e412 100644 --- a/browser/components/markdown.styl +++ b/browser/components/markdown.styl @@ -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 @@ -456,6 +486,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 pre.fence .gallery @@ -491,6 +529,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 pre.fence .gallery @@ -529,6 +575,15 @@ body[data-theme="monokai"] 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 + pre.fence .gallery .carousel-main, .carousel-footer @@ -566,6 +621,15 @@ body[data-theme="dracula"] kbd background-color themeDarkBackground + dl + border-color themeDarkBorder + background-color themeDraculaTableHead + dt + border-color themeDarkBorder + dd + border-color themeDarkBorder + background-color $ui-dracula-noteDetail-backgroundColor + pre.fence .gallery .carousel-main, .carousel-footer diff --git a/browser/lib/Languages.js b/browser/lib/Languages.js index ddb7e0ed..8c3747a9 100644 --- a/browser/lib/Languages.js +++ b/browser/lib/Languages.js @@ -48,8 +48,12 @@ const languages = [ locale: 'pl' }, { - name: 'Portuguese', - locale: 'pt' + name: 'Portuguese (PT-BR)', + locale: 'pt-BR' + }, + { + name: 'Portuguese (PT-PT)', + locale: 'pt-PT' }, { name: 'Russian', @@ -61,6 +65,9 @@ const languages = [ }, { name: 'Turkish', locale: 'tr' + }, { + name: 'Thai', + locale: 'th' } ] diff --git a/browser/lib/findNoteTitle.js b/browser/lib/findNoteTitle.js index b954f172..912c3bdd 100644 --- a/browser/lib/findNoteTitle.js +++ b/browser/lib/findNoteTitle.js @@ -1,4 +1,4 @@ -export function findNoteTitle (value) { +export function findNoteTitle (value, enableFrontMatterTitle, frontMatterTitleField = 'title') { const splitted = value.split('\n') let title = null let isInsideCodeBlock = false @@ -6,6 +6,11 @@ export function findNoteTitle (value) { if (splitted[0] === '---') { let line = 0 while (++line < splitted.length) { + if (enableFrontMatterTitle && splitted[line].startsWith(frontMatterTitleField + ':')) { + title = splitted[line].substring(frontMatterTitleField.length + 1).trim() + + break + } if (splitted[line] === '---') { splitted.splice(0, line + 1) @@ -14,17 +19,19 @@ export function findNoteTitle (value) { } } - splitted.some((line, index) => { - const trimmedLine = line.trim() - const trimmedNextLine = splitted[index + 1] === undefined ? '' : splitted[index + 1].trim() - if (trimmedLine.match('```')) { - isInsideCodeBlock = !isInsideCodeBlock - } - if (isInsideCodeBlock === false && (trimmedLine.match(/^# +/) || trimmedNextLine.match(/^=+$/))) { - title = trimmedLine - return true - } - }) + if (title === null) { + splitted.some((line, index) => { + const trimmedLine = line.trim() + const trimmedNextLine = splitted[index + 1] === undefined ? '' : splitted[index + 1].trim() + if (trimmedLine.match('```')) { + isInsideCodeBlock = !isInsideCodeBlock + } + if (isInsideCodeBlock === false && (trimmedLine.match(/^# +/) || trimmedNextLine.match(/^=+$/))) { + title = trimmedLine + return true + } + }) + } if (title === null) { title = '' diff --git a/browser/lib/markdown-it-deflist.js b/browser/lib/markdown-it-deflist.js new file mode 100644 index 00000000..db14c636 --- /dev/null +++ b/browser/lib/markdown-it-deflist.js @@ -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' ] }) +} diff --git a/browser/lib/markdown-it-sanitize-html.js b/browser/lib/markdown-it-sanitize-html.js index fb7267ee..8f6d86a8 100644 --- a/browser/lib/markdown-it-sanitize-html.js +++ b/browser/lib/markdown-it-sanitize-html.js @@ -2,6 +2,7 @@ import sanitizeHtml from 'sanitize-html' import { escapeHtmlCharacters } from './utils' +import url from 'url' module.exports = function sanitizePlugin (md, options) { options = options || {} @@ -25,7 +26,7 @@ module.exports = function sanitizePlugin (md, options) { const inlineTokens = state.tokens[tokenIdx].children for (let childIdx = 0; childIdx < inlineTokens.length; childIdx++) { if (inlineTokens[childIdx].type === 'html_inline') { - inlineTokens[childIdx].content = sanitizeHtml( + inlineTokens[childIdx].content = sanitizeInline( inlineTokens[childIdx].content, options ) @@ -35,3 +36,89 @@ module.exports = function sanitizePlugin (md, options) { } }) } + +const tagRegex = /<([A-Z][A-Z0-9]*)\s*((?:\s*[A-Z][A-Z0-9]*(?:=("|')(?:[^\3]+?)\3)?)*)\s*\/?>|<\/([A-Z][A-Z0-9]*)\s*>/i +const attributesRegex = /([A-Z][A-Z0-9]*)(?:=("|')([^\2]+?)\2)?/ig + +function sanitizeInline (html, options) { + let match = tagRegex.exec(html) + if (!match) { + return '' + } + + const { allowedTags, allowedAttributes, selfClosing, allowedSchemesAppliedToAttributes } = options + + if (match[1] !== undefined) { + // opening tag + const tag = match[1].toLowerCase() + if (allowedTags.indexOf(tag) === -1) { + return '' + } + + const attributes = match[2] + + let attrs = '' + let name + let value + + while ((match = attributesRegex.exec(attributes))) { + name = match[1].toLowerCase() + value = match[3] + + if (allowedAttributes['*'].indexOf(name) !== -1 || (allowedAttributes[tag] && allowedAttributes[tag].indexOf(name) !== -1)) { + if (allowedSchemesAppliedToAttributes.indexOf(name) !== -1) { + if (naughtyHRef(value, options) || (tag === 'iframe' && name === 'src' && naughtyIFrame(value, options))) { + continue + } + } + + attrs += ` ${name}` + if (match[2]) { + attrs += `="${value}"` + } + } + } + + if (selfClosing.indexOf(tag) === -1) { + return '<' + tag + attrs + '>' + } else { + return '<' + tag + attrs + ' />' + } + } else { + // closing tag + if (allowedTags.indexOf(match[4].toLowerCase()) !== -1) { + return html + } else { + return '' + } + } +} + +function naughtyHRef (href, options) { + // href = href.replace(/[\x00-\x20]+/g, '') + href = href.replace(/<\!\-\-.*?\-\-\>/g, '') + + const matches = href.match(/^([a-zA-Z]+)\:/) + if (!matches) { + if (href.match(/^[\/\\]{2}/)) { + return !options.allowProtocolRelative + } + + // No scheme + return false + } + + const scheme = matches[1].toLowerCase() + + return options.allowedSchemes.indexOf(scheme) === -1 +} + +function naughtyIFrame (src, options) { + try { + const parsed = url.parse(src, false, true) + + return options.allowedIframeHostnames.index(parsed.hostname) === -1 + } catch (e) { + return true + } +} diff --git a/browser/lib/markdown.js b/browser/lib/markdown.js index cf208a52..60a9fe3f 100644 --- a/browser/lib/markdown.js +++ b/browser/lib/markdown.js @@ -21,7 +21,7 @@ function createGutter (str, firstLineNumber) { class Markdown { constructor (options = {}) { - let config = ConfigManager.get() + const config = ConfigManager.get() const defaultOptions = { typographer: config.preview.smartQuotes, linkify: true, @@ -80,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 }) } @@ -123,8 +127,11 @@ class Markdown { } }) 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'), { @@ -279,9 +286,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]]) } @@ -291,9 +301,6 @@ class Markdown { } // FIXME We should not depend on global variable. window.md = this.md - this.updateConfig = () => { - config = ConfigManager.get() - } } render (content) { diff --git a/browser/main/Detail/MarkdownNoteDetail.js b/browser/main/Detail/MarkdownNoteDetail.js index e4493a80..116fdec0 100755 --- a/browser/main/Detail/MarkdownNoteDetail.js +++ b/browser/main/Detail/MarkdownNoteDetail.js @@ -61,11 +61,14 @@ class MarkdownNoteDetail extends React.Component { const reversedType = this.state.editorType === 'SPLIT' ? 'EDITOR_PREVIEW' : 'SPLIT' this.handleSwitchMode(reversedType) }) + ee.on('hotkey:deletenote', this.handleDeleteNote.bind(this)) ee.on('code:generate-toc', this.generateToc) } componentWillReceiveProps (nextProps) { - if (nextProps.note.key !== this.props.note.key && !this.state.isMovingNote) { + const isNewNote = nextProps.note.key !== this.props.note.key + const hasDeletedTags = nextProps.note.tags.length < this.props.note.tags.length + if (!this.state.isMovingNote && (isNewNote || hasDeletedTags)) { if (this.saveQueue != null) this.saveNow() this.setState({ note: Object.assign({}, nextProps.note) @@ -91,7 +94,7 @@ class MarkdownNoteDetail extends React.Component { handleUpdateContent () { const { note } = this.state note.content = this.refs.content.value - note.title = markdown.strip(striptags(findNoteTitle(note.content))) + note.title = markdown.strip(striptags(findNoteTitle(note.content, this.props.config.editor.enableFrontMatterTitle, this.props.config.editor.frontMatterTitleField))) this.updateNote(note) } @@ -293,9 +296,33 @@ class MarkdownNoteDetail extends React.Component { }) } + handleDeleteNote () { + this.handleTrashButtonClick() + } + + handleClearTodo () { + const { note } = this.state + const splitted = note.content.split('\n') + + const clearTodoContent = splitted.map((line) => { + const trimmedLine = line.trim() + if (trimmedLine.match(/\[x\]/i)) { + return line.replace(/\[x\]/i, '[ ]') + } else { + return line + } + }).join('\n') + + note.content = clearTodoContent + this.refs.content.setValue(note.content) + + this.updateNote(note) + } + renderEditor () { const { config, ignorePreviewPointerEvents } = this.props const { note } = this.state + if (this.state.editorType === 'EDITOR_PREVIEW') { return - + this.handleClearTodo(e)} percentageOfTodo={getTodoPercentageOfCompleted(note.content)} />
this.handleSwitchMode(e)} editorType={editorType} /> diff --git a/browser/main/Detail/SnippetNoteDetail.js b/browser/main/Detail/SnippetNoteDetail.js index 9356a02c..afd81102 100644 --- a/browser/main/Detail/SnippetNoteDetail.js +++ b/browser/main/Detail/SnippetNoteDetail.js @@ -112,7 +112,7 @@ class SnippetNoteDetail extends React.Component { if (this.refs.tags) note.tags = this.refs.tags.value note.description = this.refs.description.value note.updatedAt = new Date() - note.title = findNoteTitle(note.description) + note.title = findNoteTitle(note.description, false) this.setState({ note @@ -354,12 +354,10 @@ class SnippetNoteDetail extends React.Component { this.refs['code-' + this.state.snippetIndex].reload() if (this.visibleTabs.offsetWidth > this.allTabs.scrollWidth) { - console.log('no need for arrows') this.moveTabBarBy(0) } else { const lastTab = this.allTabs.lastChild if (lastTab.offsetLeft + lastTab.offsetWidth < this.visibleTabs.offsetWidth) { - console.log('need to scroll') const width = this.visibleTabs.offsetWidth const newLeft = lastTab.offsetLeft + lastTab.offsetWidth - width this.moveTabBarBy(newLeft > 0 ? -newLeft : 0) @@ -627,7 +625,6 @@ class SnippetNoteDetail extends React.Component { } focusEditor () { - console.log('code-' + this.state.snippetIndex) this.refs['code-' + this.state.snippetIndex].focus() } @@ -759,6 +756,8 @@ class SnippetNoteDetail extends React.Component { this.handleChange(e)} /> diff --git a/browser/main/Detail/TagSelect.js b/browser/main/Detail/TagSelect.js index eb160e4c..6ced475b 100644 --- a/browser/main/Detail/TagSelect.js +++ b/browser/main/Detail/TagSelect.js @@ -179,10 +179,10 @@ class TagSelect extends React.Component { } render () { - const { value, className } = this.props + const { value, className, showTagsAlphabetically } = this.props const tagList = _.isArray(value) - ? value.map((tag) => { + ? (showTagsAlphabetically ? _.sortBy(value) : value).map((tag) => { return ( ( -
-
onClick('SPLIT')}> - -
-
onClick('EDITOR_PREVIEW')}> - -
- {i18n.__('Toggle Mode')} -
-) - -ToggleModeButton.propTypes = { - onClick: PropTypes.func.isRequired, - editorType: PropTypes.string.Required -} - -export default CSSModules(ToggleModeButton, styles) +import PropTypes from 'prop-types' +import React from 'react' +import CSSModules from 'browser/lib/CSSModules' +import styles from './ToggleModeButton.styl' +import i18n from 'browser/lib/i18n' + +const ToggleModeButton = ({ + onClick, editorType +}) => ( +
+
onClick('SPLIT')}> + +
+
onClick('EDITOR_PREVIEW')}> + +
+ {i18n.__('Toggle Mode')} +
+) + +ToggleModeButton.propTypes = { + onClick: PropTypes.func.isRequired, + editorType: PropTypes.string.Required +} + +export default CSSModules(ToggleModeButton, styles) diff --git a/browser/main/Main.js b/browser/main/Main.js index 5fe9d493..e9d2c94d 100644 --- a/browser/main/Main.js +++ b/browser/main/Main.js @@ -80,7 +80,6 @@ class Main extends React.Component { } }) .then(data => { - console.log(data) store.dispatch({ type: 'ADD_STORAGE', storage: data.storage, @@ -297,7 +296,7 @@ class Main extends React.Component { onMouseUp={e => this.handleMouseUp(e)} > {!config.isSideNavFolded && diff --git a/browser/main/NoteList/index.js b/browser/main/NoteList/index.js index 30ad93c3..13117af1 100644 --- a/browser/main/NoteList/index.js +++ b/browser/main/NoteList/index.js @@ -56,7 +56,6 @@ class NoteList extends React.Component { super(props) this.selectNextNoteHandler = () => { - console.log('fired next') this.selectNextNote() } this.selectPriorNoteHandler = () => { @@ -616,7 +615,6 @@ class NoteList extends React.Component { .catch((err) => { console.error('Cannot Delete note: ' + err) }) - console.log('Notes were all deleted') } else { if (!confirmDeleteNote(confirmDeletion, false)) return @@ -636,7 +634,6 @@ class NoteList extends React.Component { }) }) AwsMobileAnalyticsConfig.recordDynamicCustomEvent('EDIT_NOTE') - console.log('Notes went to trash') }) .catch((err) => { console.error('Notes could not go to trash: ' + err) @@ -996,6 +993,7 @@ class NoteList extends React.Component { folderName={this.getNoteFolder(note).name} storageName={this.getNoteStorage(note).name} viewType={viewType} + showTagsAlphabetically={config.ui.showTagsAlphabetically} /> ) } diff --git a/browser/main/SideNav/index.js b/browser/main/SideNav/index.js index 977a8fb5..3e18095e 100644 --- a/browser/main/SideNav/index.js +++ b/browser/main/SideNav/index.js @@ -18,6 +18,11 @@ import TagButton from './TagButton' import {SortableContainer} from 'react-sortable-hoc' import i18n from 'browser/lib/i18n' import context from 'browser/lib/context' +import { remote } from 'electron' + +function matchActiveTags (tags, activeTags) { + return _.every(activeTags, v => tags.indexOf(v) >= 0) +} class SideNav extends React.Component { // TODO: should not use electron stuff v0.7 @@ -30,6 +35,52 @@ class SideNav extends React.Component { EventEmitter.off('side:preferences', this.handleMenuButtonClick) } + deleteTag (tag) { + const selectedButton = remote.dialog.showMessageBox(remote.getCurrentWindow(), { + ype: 'warning', + message: i18n.__('Confirm tag deletion'), + detail: i18n.__('This will permanently remove this tag.'), + buttons: [i18n.__('Confirm'), i18n.__('Cancel')] + }) + + if (selectedButton === 0) { + const { data, dispatch, location, params } = this.props + + const notes = data.noteMap + .map(note => note) + .filter(note => note.tags.indexOf(tag) !== -1) + .map(note => { + note = Object.assign({}, note) + note.tags = note.tags.slice() + + note.tags.splice(note.tags.indexOf(tag), 1) + + return note + }) + + Promise + .all(notes.map(note => dataApi.updateNote(note.storage, note.key, note))) + .then(updatedNotes => { + updatedNotes.forEach(note => { + dispatch({ + type: 'UPDATE_NOTE', + note + }) + }) + + if (location.pathname.match('/tags')) { + const tags = params.tagname.split(' ') + const index = tags.indexOf(tag) + if (index !== -1) { + tags.splice(index, 1) + + this.context.router.push(`/tags/${tags.map(tag => encodeURIComponent(tag)).join(' ')}`) + } + } + }) + } + } + handleMenuButtonClick (e) { openModal(PreferencesModal) } @@ -44,6 +95,17 @@ class SideNav extends React.Component { router.push('/starred') } + handleTagContextMenu (e, tag) { + const menu = [] + + menu.push({ + label: i18n.__('Delete Tag'), + click: this.deleteTag.bind(this, tag) + }) + + context.popup(menu) + } + handleToggleButtonClick (e) { const { dispatch, config } = this.props @@ -144,12 +206,20 @@ class SideNav extends React.Component { tagListComponent () { const { data, location, config } = this.props - const relatedTags = this.getRelatedTags(this.getActiveTags(location.pathname), data.noteMap) + const activeTags = this.getActiveTags(location.pathname) + const relatedTags = this.getRelatedTags(activeTags, data.noteMap) let tagList = _.sortBy(data.tagNoteMap.map( (tag, name) => ({ name, size: tag.size, related: relatedTags.has(name) }) - ), ['name']).filter( + ).filter( tag => tag.size > 0 - ) + ), ['name']) + if (config.ui.enableLiveNoteCounts && activeTags.length !== 0) { + const notesTags = data.noteMap.map(note => note.tags) + tagList = tagList.map(tag => { + tag.size = notesTags.filter(tags => tags.includes(tag.name) && matchActiveTags(tags, activeTags)).length + return tag + }) + } if (config.sortTagsBy === 'COUNTER') { tagList = _.sortBy(tagList, item => (0 - item.size)) } @@ -165,6 +235,7 @@ class SideNav extends React.Component { name={tag.name} handleClickTagListItem={this.handleClickTagListItem.bind(this)} handleClickNarrowToTag={this.handleClickNarrowToTag.bind(this)} + handleContextMenu={this.handleTagContextMenu.bind(this)} isActive={this.getTagActive(location.pathname, tag.name)} isRelated={tag.related} key={tag.name} @@ -247,7 +318,6 @@ class SideNav extends React.Component { .catch((err) => { console.error('Cannot Delete note: ' + err) }) - console.log('Trash emptied') } handleFilterButtonContextMenu (event) { diff --git a/browser/main/lib/AwsMobileAnalyticsConfig.js b/browser/main/lib/AwsMobileAnalyticsConfig.js index 1ef4f8da..e4a21a92 100644 --- a/browser/main/lib/AwsMobileAnalyticsConfig.js +++ b/browser/main/lib/AwsMobileAnalyticsConfig.js @@ -45,7 +45,6 @@ function initAwsMobileAnalytics () { if (getSendEventCond()) return AWS.config.credentials.get((err) => { if (!err) { - console.log('Cognito Identity ID: ' + AWS.config.credentials.identityId) recordDynamicCustomEvent('APP_STARTED') recordStaticCustomEvent() } @@ -58,7 +57,7 @@ function recordDynamicCustomEvent (type, options = {}) { mobileAnalyticsClient.recordEvent(type, options) } catch (analyticsError) { if (analyticsError instanceof ReferenceError) { - console.log(analyticsError.name + ': ' + analyticsError.message) + console.error(analyticsError.name + ': ' + analyticsError.message) } } } @@ -71,7 +70,7 @@ function recordStaticCustomEvent () { }) } catch (analyticsError) { if (analyticsError instanceof ReferenceError) { - console.log(analyticsError.name + ': ' + analyticsError.message) + console.error(analyticsError.name + ': ' + analyticsError.message) } } } diff --git a/browser/main/lib/ConfigManager.js b/browser/main/lib/ConfigManager.js index 1727deb8..4cbe80a7 100644 --- a/browser/main/lib/ConfigManager.js +++ b/browser/main/lib/ConfigManager.js @@ -24,7 +24,8 @@ export const DEFAULT_CONFIG = { amaEnabled: true, hotkey: { toggleMain: OSX ? 'Command + Alt + L' : 'Super + Alt + E', - toggleMode: OSX ? 'Command + Option + M' : 'Ctrl + M' + toggleMode: OSX ? 'Command + Alt + M' : 'Ctrl + M', + deleteNote: OSX ? 'Command + Shift + Backspace' : 'Ctrl + Shift + Backspace' }, ui: { language: 'en', @@ -43,11 +44,14 @@ export const DEFAULT_CONFIG = { enableRulers: false, rulers: [80, 120], displayLineNumbers: true, - switchPreview: 'BLUR', // Available value: RIGHTCLICK, BLUR + switchPreview: 'BLUR', // 'BLUR', 'DBL_CLICK', 'RIGHTCLICK' + delfaultStatus: 'PREVIEW', // 'PREVIEW', 'CODE' scrollPastEnd: false, - type: 'SPLIT', + type: 'SPLIT', // 'SPLIT', 'EDITOR_PREVIEW' fetchUrlTitle: true, - enableTableEditor: false + enableTableEditor: false, + enableFrontMatterTitle: true, + frontMatterTitleField: 'title' }, preview: { fontSize: '14', @@ -60,6 +64,7 @@ export const DEFAULT_CONFIG = { latexBlockClose: '$$', plantUMLServerAddress: 'http://www.plantuml.com/plantuml', scrollPastEnd: false, + scrollSync: true, smartQuotes: true, breaks: true, smartArrows: false, @@ -198,7 +203,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 } diff --git a/browser/main/lib/dataApi/attachmentManagement.js b/browser/main/lib/dataApi/attachmentManagement.js index a81d3bd4..c88acfc5 100644 --- a/browser/main/lib/dataApi/attachmentManagement.js +++ b/browser/main/lib/dataApi/attachmentManagement.js @@ -529,7 +529,6 @@ function handleAttachmentLinkPaste (storageKey, noteKey, linkText) { return modifiedLinkText }) } else { - console.log('One if the parameters was null -> Do nothing..') return Promise.resolve(linkText) } } diff --git a/browser/main/lib/dataApi/renameStorage.js b/browser/main/lib/dataApi/renameStorage.js index 78242bed..3b806d1c 100644 --- a/browser/main/lib/dataApi/renameStorage.js +++ b/browser/main/lib/dataApi/renameStorage.js @@ -14,7 +14,6 @@ function renameStorage (key, name) { cachedStorageList = JSON.parse(localStorage.getItem('storages')) if (!_.isArray(cachedStorageList)) throw new Error('invalid storages') } catch (err) { - console.log('error got') console.error(err) return Promise.reject(err) } diff --git a/browser/main/lib/dataApi/resolveStorageData.js b/browser/main/lib/dataApi/resolveStorageData.js index 681a102e..da41f3d0 100644 --- a/browser/main/lib/dataApi/resolveStorageData.js +++ b/browser/main/lib/dataApi/resolveStorageData.js @@ -31,13 +31,9 @@ function resolveStorageData (storageCache) { const version = parseInt(storage.version, 10) if (version >= 1) { - if (version > 1) { - console.log('The repository version is newer than one of current app.') - } return Promise.resolve(storage) } - console.log('Transform Legacy storage', storage.path) return migrateFromV6Storage(storage.path) .then(() => storage) } diff --git a/browser/main/lib/dataApi/resolveStorageNotes.js b/browser/main/lib/dataApi/resolveStorageNotes.js index fa3f19ae..9da27248 100644 --- a/browser/main/lib/dataApi/resolveStorageNotes.js +++ b/browser/main/lib/dataApi/resolveStorageNotes.js @@ -9,7 +9,7 @@ function resolveStorageNotes (storage) { notePathList = sander.readdirSync(notesDirPath) } catch (err) { if (err.code === 'ENOENT') { - console.log(notesDirPath, ' doesn\'t exist.') + console.error(notesDirPath, ' doesn\'t exist.') sander.mkdirSync(notesDirPath) } else { console.warn('Failed to find note dir', notesDirPath, err) diff --git a/browser/main/lib/dataApi/toggleStorage.js b/browser/main/lib/dataApi/toggleStorage.js index dbb625c3..246d85ef 100644 --- a/browser/main/lib/dataApi/toggleStorage.js +++ b/browser/main/lib/dataApi/toggleStorage.js @@ -12,7 +12,6 @@ function toggleStorage (key, isOpen) { cachedStorageList = JSON.parse(localStorage.getItem('storages')) if (!_.isArray(cachedStorageList)) throw new Error('invalid storages') } catch (err) { - console.log('error got') console.error(err) return Promise.reject(err) } diff --git a/browser/main/lib/eventEmitter.js b/browser/main/lib/eventEmitter.js index de08f078..1276545b 100644 --- a/browser/main/lib/eventEmitter.js +++ b/browser/main/lib/eventEmitter.js @@ -14,7 +14,6 @@ function once (name, listener) { } function emit (name, ...args) { - console.log(name) remote.getCurrentWindow().webContents.send(name, ...args) } diff --git a/browser/main/lib/ipcClient.js b/browser/main/lib/ipcClient.js index 0c916617..c06296b5 100644 --- a/browser/main/lib/ipcClient.js +++ b/browser/main/lib/ipcClient.js @@ -14,14 +14,13 @@ nodeIpc.connectTo( path.join(app.getPath('userData'), 'boostnote.service'), function () { nodeIpc.of.node.on('error', function (err) { - console.log(err) + console.error(err) }) nodeIpc.of.node.on('connect', function () { - console.log('Connected successfully') ipcRenderer.send('config-renew', {config: ConfigManager.get()}) }) nodeIpc.of.node.on('disconnect', function () { - console.log('disconnected') + return }) } ) diff --git a/browser/main/lib/shortcut.js b/browser/main/lib/shortcut.js index a6f33196..93e33c9b 100644 --- a/browser/main/lib/shortcut.js +++ b/browser/main/lib/shortcut.js @@ -3,5 +3,8 @@ import ee from 'browser/main/lib/eventEmitter' module.exports = { 'toggleMode': () => { ee.emit('topbar:togglemodebutton') + }, + 'deleteNote': () => { + ee.emit('hotkey:deletenote') } } diff --git a/browser/main/modals/PreferencesModal/Crowdfunding.js b/browser/main/modals/PreferencesModal/Crowdfunding.js index f342fb76..f6389cd8 100644 --- a/browser/main/modals/PreferencesModal/Crowdfunding.js +++ b/browser/main/modals/PreferencesModal/Crowdfunding.js @@ -23,21 +23,29 @@ class Crowdfunding extends React.Component { return (
{i18n.__('Crowdfunding')}
-

{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')}


) diff --git a/browser/main/modals/PreferencesModal/HotkeyTab.js b/browser/main/modals/PreferencesModal/HotkeyTab.js index 1c40a13a..7ad6f606 100644 --- a/browser/main/modals/PreferencesModal/HotkeyTab.js +++ b/browser/main/modals/PreferencesModal/HotkeyTab.js @@ -28,10 +28,20 @@ class HotkeyTab extends React.Component { }}) } this.handleSettingError = (err) => { - this.setState({keymapAlert: { - type: 'error', - message: err.message != null ? err.message : i18n.__('An error occurred!') - }}) + if ( + this.state.config.hotkey.toggleMain === '' || + this.state.config.hotkey.toggleMode === '' + ) { + this.setState({keymapAlert: { + type: 'success', + message: i18n.__('Successfully applied!') + }}) + } else { + this.setState({keymapAlert: { + type: 'error', + message: err.message != null ? err.message : i18n.__('An error occurred!') + }}) + } } this.oldHotkey = this.state.config.hotkey ipc.addListener('APP_SETTING_DONE', this.handleSettingDone) @@ -68,7 +78,8 @@ class HotkeyTab extends React.Component { const { config } = this.state config.hotkey = { toggleMain: this.refs.toggleMain.value, - toggleMode: this.refs.toggleMode.value + toggleMode: this.refs.toggleMode.value, + deleteNote: this.refs.deleteNote.value } this.setState({ config @@ -127,6 +138,17 @@ class HotkeyTab extends React.Component { />
+
+
{i18n.__('Delete Note')}
+
+ this.handleHotkeyChange(e)} + ref='deleteNote' + value={config.hotkey.deleteNote} + type='text' + /> +
+
-
- -
{ global.process.platform === 'win32' ?
@@ -271,6 +267,52 @@ class UiTab extends React.Component {
: null } +
Tags
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+
Editor
@@ -428,6 +470,31 @@ class UiTab extends React.Component {
+
+
+ {i18n.__('Front matter title field')} +
+
+ this.handleUIChange(e)} + type='text' + /> +
+
+ +
+ +
+
+
+ +