diff --git a/browser/components/CodeEditor.js b/browser/components/CodeEditor.js index c36a50c1..41d71622 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,6 +59,7 @@ 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() @@ -125,6 +128,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 +479,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 +548,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 +622,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 e69f312e..0af16e31 100755 --- a/browser/components/MarkdownPreview.js +++ b/browser/components/MarkdownPreview.js @@ -16,7 +16,6 @@ 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 context from 'browser/lib/context' import i18n from 'browser/lib/i18n' import fs from 'fs' @@ -80,7 +79,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 { @@ -88,6 +86,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); @@ -144,6 +147,8 @@ body p { display: none } } + +${allowCustomCSS ? customCSS : ''} ` } @@ -325,9 +330,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, @@ -484,10 +487,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 () { @@ -531,7 +530,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() @@ -737,7 +737,6 @@ export default class MarkdownPreview extends React.Component { el.addEventListener('click', this.linkClickHandler) }) } catch (e) { - console.error(e) el.className = 'flowchart-error' el.innerHTML = 'Flowchart parse error: ' + e.message } @@ -758,7 +757,6 @@ export default class MarkdownPreview extends React.Component { el.addEventListener('click', this.linkClickHandler) }) } catch (e) { - console.error(e) el.className = 'sequence-error' el.innerHTML = 'Sequence diagram parse error: ' + e.message } @@ -771,12 +769,18 @@ export default class MarkdownPreview extends React.Component { try { const chartConfig = JSON.parse(el.innerHTML) el.innerHTML = '' - var canvas = document.createElement('canvas') + + const canvas = document.createElement('canvas') el.appendChild(canvas) - /* eslint-disable no-new */ - new Chart(canvas, chartConfig) + + const height = el.attributes.getNamedItem('data-height') + if (height && height.value !== 'undefined') { + el.style.height = height.value + 'vh' + canvas.height = height.value + 'vh' + } + + const chart = new Chart(canvas, chartConfig) } catch (e) { - console.error(e) el.className = 'chart-error' el.innerHTML = 'chartjs diagram parse error: ' + e.message } @@ -860,6 +864,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 e091331b..19132622 100644 --- a/browser/components/markdown.styl +++ b/browser/components/markdown.styl @@ -209,41 +209,39 @@ code text-decoration none margin-right 2px pre - padding 0.5em !important + padding 0.5rem !important border solid 1px #D1D1D1 border-radius 5px overflow-x auto - margin 0 0 1em + margin 0 0 1rem display flex line-height 1.4em - &.flowchart, &.sequence, &.chart - display flex - justify-content center - background-color white - &.CodeMirror - height initial - flex-wrap wrap - &>code - flex 1 - overflow-x auto code background-color inherit margin 0 padding 0 border none border-radius 0 + &.CodeMirror + height initial + flex-wrap wrap + &>code + flex 1 + overflow-x auto + &.mermaid svg + max-width 100% !important &>span.filename - width 100% - border-radius: 5px 0px 0px 0px - margin -8px 100% 8px -8px - padding 0px 6px + margin -0.5rem 100% 0.5rem -0.5rem + padding 0.125rem 0.375rem background-color #777; color white + &:empty + display none &>span.lineNumber display none font-size 1em - padding 0.5em 0 - margin -0.5em 0.5em -0.5em -0.5em + padding 0.5rem 0 + margin -0.5rem 0.5rem -0.5rem -0.5rem border-right 1px solid text-align right border-top-left-radius 4px @@ -375,6 +373,19 @@ for name, val in admonition_types color: val[color] content: val[icon] +pre.fence + flex-wrap wrap + + .chart, .flowchart, .mermaid, .sequence + display flex + justify-content center + background-color white + max-width 100% + flex-grow 1 + + canvas, svg + max-width 100% !important + themeDarkBackground = darken(#21252B, 10%) themeDarkText = #f9f9f9 themeDarkBorder = lighten(themeDarkBackground, 20%) diff --git a/browser/components/render/MermaidRender.js b/browser/components/render/MermaidRender.js index e8784d9d..7a3b3ea2 100644 --- a/browser/components/render/MermaidRender.js +++ b/browser/components/render/MermaidRender.js @@ -11,9 +11,9 @@ function getRandomInt (min, max) { } function getId () { - var pool = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' - var id = 'm-' - for (var i = 0; i < 7; i++) { + const pool = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' + let id = 'm-' + for (let i = 0; i < 7; i++) { id += pool[getRandomInt(0, 16)] } return id @@ -21,16 +21,20 @@ function getId () { function render (element, content, theme) { try { + const height = element.attributes.getNamedItem('data-height') + if (height && height.value !== 'undefined') { + element.style.height = height.value + 'vh' + } let isDarkTheme = theme === 'dark' || theme === 'solarized-dark' || theme === 'monokai' || theme === 'dracula' mermaidAPI.initialize({ theme: isDarkTheme ? 'dark' : 'default', - themeCSS: isDarkTheme ? darkThemeStyling : '' + themeCSS: isDarkTheme ? darkThemeStyling : '', + useMaxWidth: false }) mermaidAPI.render(getId(), content, (svgGraph) => { element.innerHTML = svgGraph }) } catch (e) { - console.error(e) element.className = 'mermaid-error' element.innerHTML = 'mermaid diagram parse error: ' + e.message } diff --git a/browser/lib/Languages.js b/browser/lib/Languages.js index ddb7e0ed..1a798fdf 100644 --- a/browser/lib/Languages.js +++ b/browser/lib/Languages.js @@ -49,7 +49,7 @@ const languages = [ }, { name: 'Portuguese', - locale: 'pt' + locale: 'pt-BR' }, { name: 'Russian', @@ -61,6 +61,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-fence.js b/browser/lib/markdown-it-fence.js new file mode 100644 index 00000000..983dc45c --- /dev/null +++ b/browser/lib/markdown-it-fence.js @@ -0,0 +1,130 @@ +'use strict' + +module.exports = function (md, renderers, defaultRenderer) { + function fence (state, startLine, endLine) { + let pos = state.bMarks[startLine] + state.tShift[startLine] + let max = state.eMarks[startLine] + + if (state.sCount[startLine] - state.blkIndent >= 4 || pos + 3 > max) { + return false + } + + const marker = state.src.charCodeAt(pos) + if (!(marker === 96 || marker === 126)) { + return false + } + + let mem = pos + pos = state.skipChars(pos, marker) + + let len = pos - mem + if (len < 3) { + return false + } + + const markup = state.src.slice(mem, pos) + const params = state.src.slice(pos, max) + + let nextLine = startLine + let haveEndMarker = false + + while (true) { + nextLine++ + if (nextLine >= endLine) { + break + } + + pos = mem = state.bMarks[nextLine] + state.tShift[nextLine] + max = state.eMarks[nextLine] + + if (pos < max && state.sCount[nextLine] < state.blkIndent) { + break + } + if (state.src.charCodeAt(pos) !== marker || state.sCount[nextLine] - state.blkIndent >= 4) { + continue + } + + pos = state.skipChars(pos, marker) + + if (pos - mem < len) { + continue + } + + pos = state.skipSpaces(pos) + + if (pos >= max) { + haveEndMarker = true + break + } + } + + len = state.sCount[startLine] + state.line = nextLine + (haveEndMarker ? 1 : 0) + + const parameters = {} + let langType = '' + let fileName = '' + let firstLineNumber = 1 + + let match = /^(\w[-\w]*)?(?:\(((?:\s*\w[-\w]*(?:=(?:'(?:.*?[^\\])?'|"(?:.*?[^\\])?"|(?:[^'"][^\s]*)))?)*)\))?(?::([^:]*)(?::(\d+))?)?\s*$/.exec(params) + if (match) { + if (match[1]) { + langType = match[1] + } + if (match[3]) { + fileName = match[3] + } + if (match[4]) { + firstLineNumber = parseInt(match[4], 10) + } + + if (match[2]) { + const params = match[2] + const regex = /(\w[-\w]*)(?:=(?:'(.*?[^\\])?'|"(.*?[^\\])?"|([^'"][^\s]*)))?/g + + let name, value + while ((match = regex.exec(params))) { + name = match[1] + value = match[2] || match[3] || match[4] || null + + const height = /^(\d+)h$/.exec(name) + if (height && !value) { + parameters.height = height[1] + } else { + parameters[name] = value + } + } + } + } + + let token + if (renderers[langType]) { + token = state.push(`${langType}_fence`, 'div', 0) + } else { + token = state.push('_fence', 'code', 0) + } + + token.langType = langType + token.fileName = fileName + token.firstLineNumber = firstLineNumber + token.parameters = parameters + + token.content = state.getLines(startLine + 1, nextLine, len, true) + token.markup = markup + token.map = [startLine, state.line] + + return true + } + + md.block.ruler.before('fence', '_fence', fence, { + alt: ['paragraph', 'reference', 'blockquote', 'list'] + }) + + for (const name in renderers) { + md.renderer.rules[`${name}_fence`] = (tokens, index) => renderers[name](tokens[index]) + } + + if (defaultRenderer) { + md.renderer.rules['_fence'] = (tokens, index) => defaultRenderer(tokens[index]) + } +} diff --git a/browser/lib/markdown-it-sanitize-html.js b/browser/lib/markdown-it-sanitize-html.js index 05e5e7be..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 || {} @@ -14,7 +15,7 @@ module.exports = function sanitizePlugin (md, options) { options ) } - if (state.tokens[tokenIdx].type === 'fence') { + if (state.tokens[tokenIdx].type === '_fence') { // escapeHtmlCharacters has better performance state.tokens[tokenIdx].content = escapeHtmlCharacters( state.tokens[tokenIdx].content, @@ -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 248dbb4b..0c97a4d5 100644 --- a/browser/lib/markdown.js +++ b/browser/lib/markdown.js @@ -21,39 +21,13 @@ function createGutter (str, firstLineNumber) { class Markdown { constructor (options = {}) { - let config = ConfigManager.get() + const config = ConfigManager.get() const defaultOptions = { typographer: config.preview.smartQuotes, linkify: true, html: true, xhtmlOut: true, breaks: config.preview.breaks, - highlight: function (str, lang) { - const delimiter = ':' - const langInfo = lang.split(delimiter) - const langType = langInfo[0] - const fileName = langInfo[1] || '' - const firstLineNumber = parseInt(langInfo[2], 10) - - if (langType === 'flowchart') { - return `
${str}
` - } - if (langType === 'sequence') { - return `
${str}
` - } - if (langType === 'chart') { - return `
${str}
` - } - if (langType === 'mermaid') { - return `
${str}
` - } - return '
' +
-          '' + fileName + '' +
-          createGutter(str, firstLineNumber) +
-          '' +
-          str +
-          '
' - }, sanitize: 'STRICT' } @@ -106,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 }) } @@ -153,6 +131,39 @@ class Markdown { this.md.use(require('markdown-it-admonition'), {types: ['note', 'hint', 'attention', 'caution', 'danger', 'error']}) this.md.use(require('./markdown-it-frontmatter')) + this.md.use(require('./markdown-it-fence'), { + chart: token => { + return `
+          ${token.fileName}
+          
${token.content}
+
` + }, + flowchart: token => { + return `
+          ${token.fileName}
+          
${token.content}
+
` + }, + mermaid: token => { + return `
+          ${token.fileName}
+          
${token.content}
+
` + }, + sequence: token => { + return `
+          ${token.fileName}
+          
${token.content}
+
` + } + }, token => { + return `
+        ${token.fileName}
+        ${createGutter(token.content, token.firstLineNumber)}
+        ${token.content}
+      
` + }) + const deflate = require('markdown-it-plantuml/lib/deflate') this.md.use(require('markdown-it-plantuml'), '', { generateSource: function (umlCode) { @@ -265,9 +276,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..9611f21d 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, diff --git a/browser/main/lib/dataApi/attachmentManagement.js b/browser/main/lib/dataApi/attachmentManagement.js index 912450c1..c193eaf2 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' + /> +
+
+ +
+ +
+
+
+ +