diff --git a/browser/components/CodeEditor.js b/browser/components/CodeEditor.js index 29c3748b..1c9a3799 100644 --- a/browser/components/CodeEditor.js +++ b/browser/components/CodeEditor.js @@ -8,6 +8,8 @@ import copyImage from 'browser/main/lib/dataApi/copyImage' import { findStorage } from 'browser/lib/findStorage' import fs from 'fs' import eventEmitter from 'browser/main/lib/eventEmitter' +import iconv from 'iconv-lite' +const { ipcRenderer } = require('electron') CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js' @@ -32,8 +34,13 @@ export default class CodeEditor extends React.Component { constructor (props) { super(props) + this.scrollHandler = _.debounce(this.handleScroll.bind(this), 100, {leading: false, trailing: true}) this.changeHandler = (e) => this.handleChange(e) + this.focusHandler = () => { + ipcRenderer.send('editor:focused', true) + } this.blurHandler = (editor, e) => { + ipcRenderer.send('editor:focused', false) if (e == null) return null let el = e.relatedTarget while (el != null) { @@ -81,7 +88,6 @@ export default class CodeEditor extends React.Component { } } }) - this.scrollHandler = _.debounce(this.handleScroll.bind(this), 100, {leading: false, trailing: true}) } componentDidMount () { @@ -139,6 +145,7 @@ export default class CodeEditor extends React.Component { this.setMode(this.props.mode) + this.editor.on('focus', this.focusHandler) this.editor.on('blur', this.blurHandler) this.editor.on('change', this.changeHandler) this.editor.on('paste', this.pasteHandler) @@ -162,6 +169,7 @@ export default class CodeEditor extends React.Component { } componentWillUnmount () { + this.editor.off('focus', this.focusHandler) this.editor.off('blur', this.blurHandler) this.editor.off('change', this.changeHandler) this.editor.off('paste', this.pasteHandler) @@ -317,7 +325,7 @@ export default class CodeEditor extends React.Component { fetch(pastedTxt, { method: 'get' }).then((response) => { - return (response.text()) + return this.decodeResponse(response) }).then((response) => { const parsedResponse = (new window.DOMParser()).parseFromString(response, 'text/html') const value = editor.getValue() @@ -335,6 +343,31 @@ export default class CodeEditor extends React.Component { }) } + decodeResponse (response) { + const headers = response.headers + const _charset = headers.has('content-type') + ? this.extractContentTypeCharset(headers.get('content-type')) + : undefined + return response.arrayBuffer().then((buff) => { + return new Promise((resolve, reject) => { + try { + const charset = _charset !== undefined && iconv.encodingExists(_charset) ? _charset : 'utf-8' + resolve(iconv.decode(new Buffer(buff), charset).toString()) + } catch (e) { + reject(e) + } + }) + }) + } + + extractContentTypeCharset (contentType) { + return contentType.split(';').filter((str) => { + return str.trim().toLowerCase().startsWith('charset') + }).map((str) => { + return str.replace(/['"]/g, '').split('=')[1] + })[0] + } + render () { const { className, fontSize } = this.props let fontFamily = this.props.fontFamily diff --git a/browser/components/MarkdownEditor.js b/browser/components/MarkdownEditor.js index f02a146a..d0e2f505 100644 --- a/browser/components/MarkdownEditor.js +++ b/browser/components/MarkdownEditor.js @@ -279,6 +279,7 @@ class MarkdownEditor extends React.Component { lineNumber={config.preview.lineNumber} indentSize={editorIndentSize} scrollPastEnd={config.preview.scrollPastEnd} + smartQuotes={config.preview.smartQuotes} ref='preview' onContextMenu={(e) => this.handleContextMenu(e)} onDoubleClick={(e) => this.handleDoubleClick(e)} diff --git a/browser/components/MarkdownPreview.js b/browser/components/MarkdownPreview.js index c5b0355d..e4298a71 100755 --- a/browser/components/MarkdownPreview.js +++ b/browser/components/MarkdownPreview.js @@ -1,6 +1,6 @@ import PropTypes from 'prop-types' import React from 'react' -import markdown from 'browser/lib/markdown' +import Markdown from 'browser/lib/markdown' import _ from 'lodash' import CodeMirror from 'codemirror' import 'codemirror-mode-elixir' @@ -130,6 +130,13 @@ export default class MarkdownPreview extends React.Component { this.printHandler = () => this.handlePrint() this.linkClickHandler = this.handlelinkClick.bind(this) + this.initMarkdown = this.initMarkdown.bind(this) + this.initMarkdown() + } + + initMarkdown () { + const { smartQuotes } = this.props + this.markdown = new Markdown({ typographer: smartQuotes }) } handlePreviewAnchorClick (e) { @@ -198,7 +205,7 @@ export default class MarkdownPreview extends React.Component { const {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme} = this.getStyleParams() const inlineStyles = buildStyle(fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, lineNumber) - const body = markdown.render(noteContent) + const body = this.markdown.render(noteContent) const files = [this.GetCodeThemeLink(codeBlockTheme), ...CSS_FILES] files.forEach((file) => { @@ -216,6 +223,8 @@ export default class MarkdownPreview extends React.Component { return `
+ + ${styles} @@ -309,6 +318,10 @@ export default class MarkdownPreview extends React.Component { componentDidUpdate (prevProps) { if (prevProps.value !== this.props.value) this.rewriteIframe() + if (prevProps.smartQuotes !== this.props.smartQuotes) { + this.initMarkdown() + this.rewriteIframe() + } if (prevProps.fontFamily !== this.props.fontFamily || prevProps.fontSize !== this.props.fontSize || prevProps.codeBlockFontFamily !== this.props.codeBlockFontFamily || @@ -374,7 +387,7 @@ export default class MarkdownPreview extends React.Component { value = value.replace(codeBlock, htmlTextHelper.encodeEntities(codeBlock)) }) } - this.refs.root.contentWindow.document.body.innerHTML = markdown.render(value) + this.refs.root.contentWindow.document.body.innerHTML = this.markdown.render(value) _.forEach(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => { this.fixDecodedURI(el) @@ -390,9 +403,9 @@ export default class MarkdownPreview extends React.Component { }) _.forEach(this.refs.root.contentWindow.document.querySelectorAll('img'), (el) => { - el.src = markdown.normalizeLinkText(el.src) + el.src = this.markdown.normalizeLinkText(el.src) if (!/\/:storage/.test(el.src)) return - el.src = `file:///${markdown.normalizeLinkText(path.join(storagePath, 'images', path.basename(el.src)))}` + el.src = `file:///${this.markdown.normalizeLinkText(path.join(storagePath, 'images', path.basename(el.src)))}` }) codeBlockTheme = consts.THEMES.some((_theme) => _theme === codeBlockTheme) @@ -419,9 +432,9 @@ export default class MarkdownPreview extends React.Component { el.innerHTML = '' if (codeBlockTheme.indexOf('solarized') === 0) { const [refThema, color] = codeBlockTheme.split(' ') - el.parentNode.className += ` cm-s-${refThema} cm-s-${color} CodeMirror` + el.parentNode.className += ` cm-s-${refThema} cm-s-${color}` } else { - el.parentNode.className += ` cm-s-${codeBlockTheme} CodeMirror` + el.parentNode.className += ` cm-s-${codeBlockTheme}` } CodeMirror.runMode(content, syntax.mime, el, { tabSize: indentSize @@ -504,9 +517,20 @@ export default class MarkdownPreview extends React.Component { handlelinkClick (e) { const noteHash = e.target.href.split('/').pop() - const regexIsNoteLink = /^(.{20})-(.{20})$/ + // this will match the new uuid v4 hash and the old hash + // e.g. + // :note:1c211eb7dcb463de6490 and + // :note:7dd23275-f2b4-49cb-9e93-3454daf1af9c + const regexIsNoteLink = /^:note:([a-zA-Z0-9-]{20,36})$/ if (regexIsNoteLink.test(noteHash)) { - eventEmitter.emit('list:jump', noteHash) + eventEmitter.emit('list:jump', noteHash.replace(':note:', '')) + } + // this will match the old link format storage.key-note.key + // e.g. + // 877f99c3268608328037-1c211eb7dcb463de6490 + const regexIsLegacyNoteLink = /^(.{20})-(.{20})$/ + if (regexIsLegacyNoteLink.test(noteHash)) { + eventEmitter.emit('list:jump', noteHash.split('-')[1]) } } @@ -533,5 +557,6 @@ MarkdownPreview.propTypes = { className: PropTypes.string, value: PropTypes.string, showCopyNotification: PropTypes.bool, - storagePath: PropTypes.string + storagePath: PropTypes.string, + smartQuotes: PropTypes.bool } diff --git a/browser/components/MarkdownSplitEditor.js b/browser/components/MarkdownSplitEditor.js index 505fbaf4..0aa2d16c 100644 --- a/browser/components/MarkdownSplitEditor.js +++ b/browser/components/MarkdownSplitEditor.js @@ -127,6 +127,7 @@ class MarkdownSplitEditor extends React.Component { codeBlockFontFamily={config.editor.fontFamily} lineNumber={config.preview.lineNumber} scrollPastEnd={config.preview.scrollPastEnd} + smartQuotes={config.preview.smartQuotes} ref='preview' tabInde='0' value={value} diff --git a/browser/components/NoteItem.js b/browser/components/NoteItem.js index 2c93dc18..d5d3d549 100644 --- a/browser/components/NoteItem.js +++ b/browser/components/NoteItem.js @@ -62,9 +62,9 @@ const NoteItem = ({ ? 'item--active' : 'item' } - key={`${note.storage}-${note.key}`} - onClick={e => handleNoteClick(e, `${note.storage}-${note.key}`)} - onContextMenu={e => handleNoteContextMenu(e, `${note.storage}-${note.key}`)} + key={note.key} + onClick={e => handleNoteClick(e, note.key)} + onContextMenu={e => handleNoteContextMenu(e, note.key)} onDragStart={e => handleDragStart(e, note)} draggable='true' > @@ -100,7 +100,7 @@ const NoteItem = ({ {note.isStarred ?${str}`
- }
- if (langType === 'sequence') {
- return `${str}`
- }
- return '' +
- '' + fileName + '' +
- createGutter(str, firstLineNumber) +
- '' +
- str +
- ''
- }
-})
-md.use(emoji, {
- shortcuts: {}
-})
-md.use(math, {
- inlineOpen: config.preview.latexInlineOpen,
- inlineClose: config.preview.latexInlineClose,
- blockOpen: config.preview.latexBlockOpen,
- blockClose: config.preview.latexBlockClose,
- inlineRenderer: function (str) {
- let output = ''
- try {
- output = katex.renderToString(str.trim())
- } catch (err) {
- output = `${err.message}`
- }
- return output
- },
- blockRenderer: function (str) {
- let output = ''
- try {
- output = katex.renderToString(str.trim(), { displayMode: true })
- } catch (err) {
- output = `${str}`
}
- liToken.attrs.push(['class', 'taskListItem'])
+ if (langType === 'sequence') {
+ return `${str}`
+ }
+ return '' +
+ '' + fileName + '' +
+ createGutter(str, firstLineNumber) +
+ '' +
+ str +
+ ''
}
- content = ``
}
+
+ const updatedOptions = Object.assign(defaultOptions, options)
+ this.md = markdownit(updatedOptions)
+
+ // Sanitize use rinput before other plugins
+ this.md.use(sanitize, {
+ allowedTags: ['iframe', 'input', 'b',
+ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'h7', 'h8', 'br', 'b', 'i', 'strong', 'em', 'a', 'pre', 'code', 'img', 'tt',
+ 'div', 'ins', 'del', 'sup', 'sub', 'p', 'ol', 'ul', 'table', 'thead', 'tbody', 'tfoot', 'blockquote',
+ 'dl', 'dt', 'dd', 'kbd', 'q', 'samp', 'var', 'hr', 'ruby', 'rt', 'rp', 'li', 'tr', 'td', 'th', 's', 'strike', 'summary', 'details'
+ ],
+ allowedAttributes: {
+ '*': [
+ 'style',
+ 'abbr', 'accept', 'accept-charset',
+ 'accesskey', 'action', 'align', 'alt', 'axis',
+ 'border', 'cellpadding', 'cellspacing', 'char',
+ 'charoff', 'charset', 'checked',
+ 'clear', 'cols', 'colspan', 'color',
+ 'compact', 'coords', 'datetime', 'dir',
+ 'disabled', 'enctype', 'for', 'frame',
+ 'headers', 'height', 'hreflang',
+ 'hspace', 'ismap', 'label', 'lang',
+ 'maxlength', 'media', 'method',
+ 'multiple', 'name', 'nohref', 'noshade',
+ 'nowrap', 'open', 'prompt', 'readonly', 'rel', 'rev',
+ 'rows', 'rowspan', 'rules', 'scope',
+ 'selected', 'shape', 'size', 'span',
+ 'start', 'summary', 'tabindex', 'target',
+ 'title', 'type', 'usemap', 'valign', 'value',
+ 'vspace', 'width', 'itemprop'
+ ],
+ 'a': ['href'],
+ 'div': ['itemscope', 'itemtype'],
+ 'blockquote': ['cite'],
+ 'del': ['cite'],
+ 'ins': ['cite'],
+ 'q': ['cite'],
+ 'img': ['src', 'width', 'height'],
+ 'iframe': ['src', 'width', 'height', 'frameborder', 'allowfullscreen'],
+ 'input': ['type', 'id', 'checked']
+ },
+ allowedIframeHostnames: ['www.youtube.com']
+ })
+
+ this.md.use(emoji, {
+ shortcuts: {}
+ })
+ this.md.use(math, {
+ inlineOpen: config.preview.latexInlineOpen,
+ inlineClose: config.preview.latexInlineClose,
+ blockOpen: config.preview.latexBlockOpen,
+ blockClose: config.preview.latexBlockClose,
+ inlineRenderer: function (str) {
+ let output = ''
+ try {
+ output = katex.renderToString(str.trim())
+ } catch (err) {
+ output = `${err.message}`
+ }
+ return output
+ },
+ blockRenderer: function (str) {
+ let output = ''
+ try {
+ output = katex.renderToString(str.trim(), { displayMode: true })
+ } catch (err) {
+ output = `{updatedAt}
-MODIFICATION DATE
+{i18n.__('MODIFICATION DATE')}
{wordCount}
-Words
+{i18n.__('Words')}
{letterCount}
-Letters
+{i18n.__('Letters')}
{storageName}
-STORAGE
+{i18n.__('STORAGE')}
{folderName}
-FOLDER
+{i18n.__('FOLDER')}
{createdAt}
-CREATION DATE
+{i18n.__('CREATION DATE')}
NOTE LINK
+{i18n.__('NOTE LINK')}
.md
+{i18n.__('.md')}
.txt
+{i18n.__('.txt')}
.html
+{i18n.__('.html')}
{i18n.__('Print')}
{updatedAt}
-MODIFICATION DATE
+{i18n.__('MODIFICATION DATE')}
{storageName}
-STORAGE
+{i18n.__('STORAGE')}
FOLDER
+{i18n.__('FOLDER')}
{createdAt}
-CREATION DATE
+{i18n.__('CREATION DATE')}
+ {BlogAlert.message} +
+ : null + return ( +Dear everyone,
+{i18n.__('Dear everyone,')}
Thank you for using Boostnote!
-Boostnote is used in about 200 different countries and regions by an awesome community of developers.
+{i18n.__('Thank you for using Boostnote!')}
+{i18n.__('Boostnote is used in about 200 different countries and regions by an awesome community of developers.')}
To continue supporting this growth, and to satisfy community expectations,
-we would like to invest more time and resources in this project.
+{i18n.__('To continue supporting this growth, and to satisfy community expectations,')}
+{i18n.__('we would like to invest more time and resources in this project.')}
If you like this project and see its potential, you can help by supporting us on OpenCollective!
+{i18n.__('If you like this project and see its potential, you can help by supporting us on OpenCollective!')}
Thanks,
-Boostnote maintainers
+{i18n.__('Thanks,')}
+{i18n.__('Boostnote maintainers')}