diff --git a/.babelrc b/.babelrc index 270349d2..3a366286 100644 --- a/.babelrc +++ b/.babelrc @@ -7,7 +7,7 @@ "test": { "presets": ["env" ,"react", "es2015"], "plugins": [ - [ "babel-plugin-webpack-alias", { "config": "${PWD}/webpack.config.js" } ] + [ "babel-plugin-webpack-alias", { "config": "/webpack.config.js" } ] ] } } diff --git a/.eslintrc b/.eslintrc index 1709c9d8..be8cb903 100644 --- a/.eslintrc +++ b/.eslintrc @@ -18,7 +18,9 @@ "globals": { "FileReader": true, "localStorage": true, - "fetch": true + "fetch": true, + "Image": true, + "MutationObserver": true }, "env": { "jest": true diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..ea304082 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +issuehunt: BoostIo/Boostnote diff --git a/.gitignore b/.gitignore index ace5316c..aac64950 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,5 @@ node_modules/* /secret *.log .idea -.vscode \ No newline at end of file +.vscode +package-lock.json diff --git a/.travis.yml b/.travis.yml index 90548ee9..71b65671 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,6 @@ node_js: - 8 script: - npm run lint && npm run test - - yarn jest - 'if [[ ${TRAVIS_PULL_REQUEST_BRANCH:-$TRAVIS_BRANCH} = "master" ]]; then npm install -g grunt npm@6.4 && grunt pre-build; fi' after_success: - openssl aes-256-cbc -K $encrypted_440d7f9a3c38_key -iv $encrypted_440d7f9a3c38_iv diff --git a/browser/components/CodeEditor.js b/browser/components/CodeEditor.js index 4893b342..7080b1fc 100644 --- a/browser/components/CodeEditor.js +++ b/browser/components/CodeEditor.js @@ -20,12 +20,15 @@ import styles from '../components/CodeEditor.styl' const { ipcRenderer, remote, clipboard } = require('electron') import normalizeEditorFontFamily from 'browser/lib/normalizeEditorFontFamily' const spellcheck = require('browser/lib/spellcheck') -const buildEditorContextMenu = require('browser/lib/contextMenuBuilder') -import TurndownService from 'turndown' +const buildEditorContextMenu = require('browser/lib/contextMenuBuilder').buildEditorContextMenu +import { createTurndownService } from '../lib/turndown' import {languageMaps} from '../lib/CMLanguageList' import snippetManager from '../lib/SnippetManager' import {generateInEditor, tocExistsInEditor} from 'browser/lib/markdown-toc-generator' import markdownlint from 'markdownlint' +import Jsonlint from 'jsonlint-mod' +import { DEFAULT_CONFIG } from '../main/lib/ConfigManager' +import prettier from 'prettier' CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js' @@ -38,38 +41,6 @@ function translateHotkey (hotkey) { return hotkey.replace(/\s*\+\s*/g, '-').replace(/Command/g, 'Cmd').replace(/Control/g, 'Ctrl') } -const validatorOfMarkdown = (text, updateLinting) => { - const lintOptions = { - 'strings': { - 'content': text - } - } - - return markdownlint(lintOptions, (err, result) => { - if (!err) { - const foundIssues = [] - result.content.map(item => { - let ruleNames = '' - item.ruleNames.map((ruleName, index) => { - ruleNames += ruleName - if (index === item.ruleNames.length - 1) { - ruleNames += ': ' - } else { - ruleNames += '/' - } - }) - foundIssues.push({ - from: CodeMirror.Pos(item.lineNumber, 0), - to: CodeMirror.Pos(item.lineNumber, 1), - message: ruleNames + item.ruleDescription, - severity: 'warning' - }) - }) - updateLinting(foundIssues) - } - }) -} - export default class CodeEditor extends React.Component { constructor (props) { super(props) @@ -83,6 +54,7 @@ export default class CodeEditor extends React.Component { this.focusHandler = () => { ipcRenderer.send('editor:focused', true) } + const debouncedDeletionOfAttachments = _.debounce(attachmentManagement.deleteAttachmentsNotPresentInNote, 30000) this.blurHandler = (editor, e) => { ipcRenderer.send('editor:focused', false) if (e == null) return null @@ -94,16 +66,13 @@ export default class CodeEditor extends React.Component { el = el.parentNode } this.props.onBlur != null && this.props.onBlur(e) - const { storageKey, noteKey } = this.props - attachmentManagement.deleteAttachmentsNotPresentInNote( - this.editor.getValue(), - storageKey, - noteKey - ) + if (this.props.deleteUnusedAttachments === true) { + debouncedDeletionOfAttachments(this.editor.getValue(), storageKey, noteKey) + } } this.pasteHandler = (editor, e) => { e.preventDefault() @@ -116,6 +85,8 @@ export default class CodeEditor extends React.Component { this.searchHandler = (e, msg) => this.handleSearch(msg) this.searchState = null this.scrollToLineHandeler = this.scrollToLine.bind(this) + this.getCodeEditorLintConfig = this.getCodeEditorLintConfig.bind(this) + this.validatorOfMarkdown = this.validatorOfMarkdown.bind(this) this.formatTable = () => this.handleFormatTable() @@ -130,7 +101,7 @@ export default class CodeEditor extends React.Component { this.editorActivityHandler = () => this.handleEditorActivity() - this.turndownService = new TurndownService() + this.turndownService = createTurndownService() } handleSearch (msg) { @@ -138,7 +109,7 @@ export default class CodeEditor extends React.Component { const component = this if (component.searchState) cm.removeOverlay(component.searchState) - if (msg.length < 3) return + if (msg.length < 1) return cm.operation(function () { component.searchState = makeOverlay(msg, 'searching') @@ -233,23 +204,11 @@ export default class CodeEditor extends React.Component { 'Cmd-T': function (cm) { // Do nothing }, - 'Ctrl-/': function (cm) { - if (global.process.platform === 'darwin') { return } + [translateHotkey(hotkey.insertDate)]: function (cm) { const dateNow = new Date() cm.replaceSelection(dateNow.toLocaleDateString()) }, - 'Cmd-/': function (cm) { - if (global.process.platform !== 'darwin') { return } - const dateNow = new Date() - cm.replaceSelection(dateNow.toLocaleDateString()) - }, - 'Shift-Ctrl-/': function (cm) { - if (global.process.platform === 'darwin') { return } - const dateNow = new Date() - cm.replaceSelection(dateNow.toLocaleString()) - }, - 'Shift-Cmd-/': function (cm) { - if (global.process.platform !== 'darwin') { return } + [translateHotkey(hotkey.insertDateTime)]: function (cm) { const dateNow = new Date() cm.replaceSelection(dateNow.toLocaleString()) }, @@ -260,6 +219,37 @@ export default class CodeEditor extends React.Component { } return CodeMirror.Pass }, + [translateHotkey(hotkey.prettifyMarkdown)]: cm => { + // Default / User configured prettier options + const currentConfig = JSON.parse(self.props.prettierConfig) + + // Parser type will always need to be markdown so we override the option before use + currentConfig.parser = 'markdown' + + // Get current cursor position + const cursorPos = cm.getCursor() + currentConfig.cursorOffset = cm.doc.indexFromPos(cursorPos) + + // Prettify contents of editor + const formattedTextDetails = prettier.formatWithCursor(cm.doc.getValue(), currentConfig) + + const formattedText = formattedTextDetails.formatted + const formattedCursorPos = formattedTextDetails.cursorOffset + cm.doc.setValue(formattedText) + + // Reset Cursor position to be at the same markdown as was before prettifying + const newCursorPos = cm.doc.posFromIndex(formattedCursorPos) + cm.doc.setCursor(newCursorPos) + }, + [translateHotkey(hotkey.sortLines)]: cm => { + const selection = cm.doc.getSelection() + const appendLineBreak = /\n$/.test(selection) + + const sorted = _.split(selection.trim(), '\n').sort() + const sortedString = _.join(sorted, '\n') + (appendLineBreak ? '\n' : '') + + cm.doc.replaceSelection(sortedString) + }, [translateHotkey(hotkey.pasteSmartly)]: cm => { this.handlePaste(cm, true) } @@ -283,20 +273,19 @@ export default class CodeEditor extends React.Component { } componentDidMount () { - const { rulers, enableRulers } = this.props + const { rulers, enableRulers, enableMarkdownLint } = this.props eventEmitter.on('line:jump', this.scrollToLineHandeler) snippetManager.init() this.updateDefaultKeyMap() - const checkMarkdownNoteIsOpening = this.props.mode === 'Boost Flavored Markdown' this.value = this.props.value this.editor = CodeMirror(this.refs.root, { rulers: buildCMRulers(rulers, enableRulers), value: this.props.value, linesHighlighted: this.props.linesHighlighted, lineNumbers: this.props.displayLineNumbers, - lineWrapping: true, + lineWrapping: this.props.lineWrapping, theme: this.props.theme, indentUnit: this.props.indentSize, tabSize: this.props.indentSize, @@ -306,10 +295,7 @@ export default class CodeEditor extends React.Component { inputStyle: 'textarea', dragDrop: false, foldGutter: true, - lint: checkMarkdownNoteIsOpening ? { - 'getAnnotations': validatorOfMarkdown, - 'async': true - } : false, + lint: enableMarkdownLint ? this.getCodeEditorLintConfig() : false, gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter', 'CodeMirror-lint-markers'], autoCloseBrackets: { pairs: this.props.matchingPairs, @@ -317,9 +303,12 @@ export default class CodeEditor extends React.Component { explode: this.props.explodingPairs, override: true }, - extraKeys: this.defaultKeyMap + extraKeys: this.defaultKeyMap, + prettierConfig: this.props.prettierConfig }) + document.querySelector('.CodeMirror-lint-markers').style.display = enableMarkdownLint ? 'inline-block' : 'none' + if (!this.props.mode && this.props.value && this.props.autoDetect) { this.autoDetectLanguage(this.props.value) } else { @@ -546,7 +535,9 @@ export default class CodeEditor extends React.Component { let needRefresh = false const { rulers, - enableRulers + enableRulers, + enableMarkdownLint, + customMarkdownLintConfig } = this.props if (prevProps.mode !== this.props.mode) { this.setMode(this.props.mode) @@ -564,6 +555,16 @@ export default class CodeEditor extends React.Component { if (prevProps.keyMap !== this.props.keyMap) { needRefresh = true } + if (prevProps.enableMarkdownLint !== enableMarkdownLint || prevProps.customMarkdownLintConfig !== customMarkdownLintConfig) { + if (!enableMarkdownLint) { + this.editor.setOption('lint', {default: false}) + document.querySelector('.CodeMirror-lint-markers').style.display = 'none' + } else { + this.editor.setOption('lint', this.getCodeEditorLintConfig()) + document.querySelector('.CodeMirror-lint-markers').style.display = 'inline-block' + } + needRefresh = true + } if ( prevProps.enableRulers !== enableRulers || @@ -584,6 +585,10 @@ export default class CodeEditor extends React.Component { this.editor.setOption('lineNumbers', this.props.displayLineNumbers) } + if (prevProps.lineWrapping !== this.props.lineWrapping) { + this.editor.setOption('lineWrapping', this.props.lineWrapping) + } + if (prevProps.scrollPastEnd !== this.props.scrollPastEnd) { this.editor.setOption('scrollPastEnd', this.props.scrollPastEnd) } @@ -638,12 +643,65 @@ export default class CodeEditor extends React.Component { this.editor.addPanel(this.createSpellCheckPanel(), {position: 'bottom'}) } } + if (prevProps.deleteUnusedAttachments !== this.props.deleteUnusedAttachments) { + this.editor.setOption('deleteUnusedAttachments', this.props.deleteUnusedAttachments) + } if (needRefresh) { this.editor.refresh() } } + getCodeEditorLintConfig () { + const { mode } = this.props + const checkMarkdownNoteIsOpen = mode === 'Boost Flavored Markdown' + + return checkMarkdownNoteIsOpen ? { + getAnnotations: this.validatorOfMarkdown, + async: true + } : false + } + + validatorOfMarkdown (text, updateLinting) { + const { customMarkdownLintConfig } = this.props + let lintConfigJson + try { + Jsonlint.parse(customMarkdownLintConfig) + lintConfigJson = JSON.parse(customMarkdownLintConfig) + } catch (err) { + eventEmitter.emit('APP_SETTING_ERROR') + return + } + const lintOptions = { + strings: { + content: text + }, + config: lintConfigJson + } + + return markdownlint(lintOptions, (err, result) => { + if (!err) { + const foundIssues = [] + const splitText = text.split('\n') + result.content.map(item => { + let ruleNames = '' + item.ruleNames.map((ruleName, index) => { + ruleNames += ruleName + ruleNames += (index === item.ruleNames.length - 1) ? ': ' : '/' + }) + const lineNumber = item.lineNumber - 1 + foundIssues.push({ + from: CodeMirror.Pos(lineNumber, 0), + to: CodeMirror.Pos(lineNumber, splitText[lineNumber].length), + message: ruleNames + item.ruleDescription, + severity: 'warning' + }) + }) + updateLinting(foundIssues) + } + }) + } + setMode (mode) { let syntax = CodeMirror.findModeByName(convertModeName(mode || 'text')) if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text') @@ -816,6 +874,17 @@ export default class CodeEditor extends React.Component { this.editor.setCursor(cursor) } + /** + * Update content of one line + * @param {Number} lineNumber + * @param {String} content + */ + setLineContent (lineNumber, content) { + const prevContent = this.editor.getLine(lineNumber) + const prevContentLength = prevContent ? prevContent.length : 0 + this.editor.replaceRange(content, { line: lineNumber, ch: 0 }, { line: lineNumber, ch: prevContentLength }) + } + handleDropImage (dropEvent) { dropEvent.preventDefault() const { @@ -1105,13 +1174,11 @@ export default class CodeEditor extends React.Component { } ref='root' tabIndex='-1' - style={ - { + style={{ fontFamily, fontSize: fontSize, width: width - } - } + }} onDrop={ e => this.handleDropImage(e) } @@ -1149,7 +1216,10 @@ CodeEditor.propTypes = { onChange: PropTypes.func, readOnly: PropTypes.bool, autoDetect: PropTypes.bool, - spellCheck: PropTypes.bool + spellCheck: PropTypes.bool, + enableMarkdownLint: PropTypes.bool, + customMarkdownLintConfig: PropTypes.string, + deleteUnusedAttachments: PropTypes.bool } CodeEditor.defaultProps = { @@ -1161,5 +1231,9 @@ CodeEditor.defaultProps = { indentSize: 4, indentType: 'space', autoDetect: false, - spellCheck: false + spellCheck: false, + enableMarkdownLint: DEFAULT_CONFIG.editor.enableMarkdownLint, + customMarkdownLintConfig: DEFAULT_CONFIG.editor.customMarkdownLintConfig, + prettierConfig: DEFAULT_CONFIG.editor.prettierConfig, + deleteUnusedAttachments: DEFAULT_CONFIG.editor.deleteUnusedAttachments } diff --git a/browser/components/MarkdownEditor.js b/browser/components/MarkdownEditor.js index 593f7d99..5c2ddbdb 100644 --- a/browser/components/MarkdownEditor.js +++ b/browser/components/MarkdownEditor.js @@ -119,7 +119,7 @@ class MarkdownEditor extends React.Component { status: 'PREVIEW' }, () => { this.refs.preview.focus() - this.refs.preview.scrollTo(cursorPosition.line) + this.refs.preview.scrollToRow(cursorPosition.line) }) eventEmitter.emit('topbar:togglelockbutton', this.state.status) } @@ -159,24 +159,25 @@ class MarkdownEditor extends React.Component { e.preventDefault() e.stopPropagation() const idMatch = /checkbox-([0-9]+)/ - const checkedMatch = /^\s*[\+\-\*] \[x\]/i - const uncheckedMatch = /^\s*[\+\-\*] \[ \]/ - const checkReplace = /\[x\]/i - const uncheckReplace = /\[ \]/ + const checkedMatch = /^(\s*>?)*\s*[+\-*] \[x]/i + const uncheckedMatch = /^(\s*>?)*\s*[+\-*] \[ ]/ + const checkReplace = /\[x]/i + const uncheckReplace = /\[ ]/ if (idMatch.test(e.target.getAttribute('id'))) { const lineIndex = parseInt(e.target.getAttribute('id').match(idMatch)[1], 10) - 1 const lines = this.refs.code.value .split('\n') const targetLine = lines[lineIndex] + let newLine = targetLine if (targetLine.match(checkedMatch)) { - lines[lineIndex] = targetLine.replace(checkReplace, '[ ]') + newLine = targetLine.replace(checkReplace, '[ ]') } if (targetLine.match(uncheckedMatch)) { - lines[lineIndex] = targetLine.replace(uncheckReplace, '[x]') + newLine = targetLine.replace(uncheckReplace, '[x]') } - this.refs.code.setValue(lines.join('\n')) + this.refs.code.setLineContent(lineIndex, newLine) } } @@ -304,6 +305,7 @@ class MarkdownEditor extends React.Component { enableRulers={config.editor.enableRulers} rulers={config.editor.rulers} displayLineNumbers={config.editor.displayLineNumbers} + lineWrapping matchingPairs={config.editor.matchingPairs} matchingTriples={config.editor.matchingTriples} explodingPairs={config.editor.explodingPairs} @@ -319,6 +321,10 @@ class MarkdownEditor extends React.Component { enableSmartPaste={config.editor.enableSmartPaste} hotkey={config.hotkey} switchPreview={config.editor.switchPreview} + enableMarkdownLint={config.editor.enableMarkdownLint} + customMarkdownLintConfig={config.editor.customMarkdownLintConfig} + prettierConfig={config.editor.prettierConfig} + deleteUnusedAttachments={config.editor.deleteUnusedAttachments} /> this.handleContextMenu(e)} onDoubleClick={(e) => this.handleDoubleClick(e)} diff --git a/browser/components/MarkdownPreview.js b/browser/components/MarkdownPreview.js index 55b36243..7c88f562 100755 --- a/browser/components/MarkdownPreview.js +++ b/browser/components/MarkdownPreview.js @@ -18,15 +18,13 @@ import mdurl from 'mdurl' import exportNote from 'browser/main/lib/dataApi/exportNote' import { escapeHtmlCharacters } from 'browser/lib/utils' import yaml from 'js-yaml' -import context from 'browser/lib/context' -import i18n from 'browser/lib/i18n' -import fs from 'fs' import { render } from 'react-dom' import Carousel from 'react-image-carousel' import ConfigManager from '../main/lib/ConfigManager' const { remote, shell } = require('electron') const attachmentManagement = require('../main/lib/dataApi/attachmentManagement') +const buildMarkdownPreviewContextMenu = require('browser/lib/contextMenuBuilder').buildMarkdownPreviewContextMenu const { app } = remote const path = require('path') @@ -34,8 +32,6 @@ const fileUrl = require('file-url') const dialog = remote.dialog -const uri2path = require('file-uri-to-path') - const markdownStyle = require('!!css!stylus?sourceMap!./markdown.styl')[0][1] const appPath = fileUrl( process.env.NODE_ENV === 'production' ? app.getAppPath() : path.resolve() @@ -46,16 +42,29 @@ const CSS_FILES = [ `${appPath}/node_modules/react-image-carousel/lib/css/main.min.css` ] -function buildStyle ( - fontFamily, - fontSize, - codeBlockFontFamily, - lineNumber, - scrollPastEnd, - theme, - allowCustomCSS, - customCSS -) { +/** + * @param {Object} opts + * @param {String} opts.fontFamily + * @param {Numberl} opts.fontSize + * @param {String} opts.codeBlockFontFamily + * @param {String} opts.theme + * @param {Boolean} [opts.lineNumber] Should show line number + * @param {Boolean} [opts.scrollPastEnd] + * @param {Boolean} [opts.allowCustomCSS] Should add custom css + * @param {String} [opts.customCSS] Will be added to bottom, only if `opts.allowCustomCSS` is truthy + * @returns {String} + */ +function buildStyle (opts) { + const { + fontFamily, + fontSize, + codeBlockFontFamily, + lineNumber, + scrollPastEnd, + theme, + allowCustomCSS, + customCSS + } = opts return ` @font-face { font-family: 'Lato'; @@ -85,12 +94,17 @@ function buildStyle ( url('${appPath}/resources/fonts/MaterialIcons-Regular.woff') format('woff'), url('${appPath}/resources/fonts/MaterialIcons-Regular.ttf') format('truetype'); } + ${markdownStyle} body { font-family: '${fontFamily.join("','")}'; font-size: ${fontSize}px; - ${scrollPastEnd && 'padding-bottom: 90vh;'} + ${scrollPastEnd ? ` + padding-bottom: 90vh; + box-sizing: border-box; + ` + : ''} } @media print { body { @@ -166,6 +180,10 @@ const scrollBarStyle = ` ::-webkit-scrollbar-thumb { background-color: rgba(0, 0, 0, 0.15); } + +::-webkit-scrollbar-track-piece { + background-color: inherit; +} ` const scrollBarDarkStyle = ` ::-webkit-scrollbar { @@ -175,6 +193,10 @@ const scrollBarDarkStyle = ` ::-webkit-scrollbar-thumb { background-color: rgba(0, 0, 0, 0.3); } + +::-webkit-scrollbar-track-piece { + background-color: inherit; +} ` const OSX = global.process.platform === 'darwin' @@ -249,30 +271,12 @@ export default class MarkdownPreview extends React.Component { } handleContextMenu (event) { - // If a contextMenu handler was passed to us, use it instead of the self-defined one -> return - if (_.isFunction(this.props.onContextMenu)) { + const menu = buildMarkdownPreviewContextMenu(this, event) + const switchPreview = ConfigManager.get().editor.switchPreview + if (menu != null && switchPreview !== 'RIGHTCLICK') { + menu.popup(remote.getCurrentWindow()) + } else if (_.isFunction(this.props.onContextMenu)) { this.props.onContextMenu(event) - return - } - // No contextMenu was passed to us -> execute our own link-opener - if (event.target.tagName.toLowerCase() === 'a' && event.target.getAttribute('href')) { - const href = event.target.href - const isLocalFile = href.startsWith('file:') - if (isLocalFile) { - const absPath = uri2path(href) - try { - if (fs.lstatSync(absPath).isFile()) { - context.popup([ - { - label: i18n.__('Show in explorer'), - click: (e) => shell.showItemInFolder(absPath) - } - ]) - } - } catch (e) { - console.log('Error while evaluating if the file is locally available', e) - } - } } } @@ -334,7 +338,7 @@ export default class MarkdownPreview extends React.Component { customCSS } = this.getStyleParams() - const inlineStyles = buildStyle( + const inlineStyles = buildStyle({ fontFamily, fontSize, codeBlockFontFamily, @@ -343,9 +347,13 @@ export default class MarkdownPreview extends React.Component { theme, allowCustomCSS, customCSS - ) + }) let body = this.markdown.render(noteContent) - const files = [this.GetCodeThemeLink(codeBlockTheme), ...CSS_FILES] + body = attachmentManagement.fixLocalURLS( + body, + this.props.storagePath + ) + const files = [this.getCodeThemeLink(codeBlockTheme), ...CSS_FILES] files.forEach(file => { if (global.process.platform === 'win32') { file = file.replace('file:///', '') @@ -381,7 +389,7 @@ export default class MarkdownPreview extends React.Component { handleSaveAsPdf () { this.exportAsDocument('pdf', (noteContent, exportTasks, targetDir) => { - const printout = new remote.BrowserWindow({show: false, webPreferences: {webSecurity: false}}) + const printout = new remote.BrowserWindow({show: false, webPreferences: {webSecurity: false, javascript: false}}) printout.loadURL('data:text/html;charset=UTF-8,' + this.htmlContentFormatter(noteContent, exportTasks, targetDir)) return new Promise((resolve, reject) => { printout.webContents.on('did-finish-load', () => { @@ -576,16 +584,19 @@ export default class MarkdownPreview extends React.Component { } componentDidUpdate (prevProps) { - if (prevProps.value !== this.props.value) this.rewriteIframe() + // actual rewriteIframe function should be called only once + let needsRewriteIframe = false + if (prevProps.value !== this.props.value) needsRewriteIframe = true if ( prevProps.smartQuotes !== this.props.smartQuotes || prevProps.sanitize !== this.props.sanitize || + prevProps.mermaidHTMLLabel !== this.props.mermaidHTMLLabel || prevProps.smartArrows !== this.props.smartArrows || prevProps.breaks !== this.props.breaks || prevProps.lineThroughCheckbox !== this.props.lineThroughCheckbox ) { this.initMarkdown() - this.rewriteIframe() + needsRewriteIframe = true } if ( prevProps.fontFamily !== this.props.fontFamily || @@ -600,8 +611,17 @@ export default class MarkdownPreview extends React.Component { prevProps.customCSS !== this.props.customCSS ) { this.applyStyle() + needsRewriteIframe = true + } + + if (needsRewriteIframe) { this.rewriteIframe() } + + // Should scroll to top after selecting another note + if (prevProps.noteKey !== this.props.noteKey) { + this.scrollTo(0, 0) + } } getStyleParams () { @@ -657,8 +677,8 @@ export default class MarkdownPreview extends React.Component { this.getWindow().document.getElementById( 'codeTheme' - ).href = this.GetCodeThemeLink(codeBlockTheme) - this.getWindow().document.getElementById('style').innerHTML = buildStyle( + ).href = this.getCodeThemeLink(codeBlockTheme) + this.getWindow().document.getElementById('style').innerHTML = buildStyle({ fontFamily, fontSize, codeBlockFontFamily, @@ -667,17 +687,15 @@ export default class MarkdownPreview extends React.Component { theme, allowCustomCSS, customCSS - ) + }) } - GetCodeThemeLink (name) { + getCodeThemeLink (name) { const theme = consts.THEMES.find(theme => theme.name === name) - if (theme) { - return `${appPath}/${theme.path}` - } else { - return `${appPath}/node_modules/codemirror/theme/elegant.css` - } + return theme != null + ? theme.path + : `${appPath}/node_modules/codemirror/theme/elegant.css` } rewriteIframe () { @@ -703,7 +721,8 @@ export default class MarkdownPreview extends React.Component { showCopyNotification, storagePath, noteKey, - sanitize + sanitize, + mermaidHTMLLabel } = this.props let { value, codeBlockTheme } = this.props @@ -835,6 +854,7 @@ export default class MarkdownPreview extends React.Component { canvas.height = height.value + 'vh' } + // eslint-disable-next-line no-unused-vars const chart = new Chart(canvas, chartConfig) } catch (e) { el.className = 'chart-error' @@ -845,7 +865,7 @@ export default class MarkdownPreview extends React.Component { _.forEach( this.refs.root.contentWindow.document.querySelectorAll('.mermaid'), el => { - mermaidRender(el, htmlTextHelper.decodeEntities(el.innerHTML), theme) + mermaidRender(el, htmlTextHelper.decodeEntities(el.innerHTML), theme, mermaidHTMLLabel) } ) @@ -895,6 +915,12 @@ export default class MarkdownPreview extends React.Component { this.setImgOnClickEventHelper(img, rect) imgObserver.observe(parentEl, config) } + + const aList = markdownPreviewIframe.contentWindow.document.body.querySelectorAll('a') + for (const a of aList) { + a.removeEventListener('click', this.linkClickHandler) + a.addEventListener('click', this.linkClickHandler) + } } setImgOnClickEventHelper (img, rect) { @@ -967,8 +993,6 @@ export default class MarkdownPreview extends React.Component { overlay.appendChild(zoomImg) document.body.appendChild(overlay) } - - this.getWindow().scrollTo(0, 0) } focus () { @@ -979,7 +1003,11 @@ export default class MarkdownPreview extends React.Component { return this.refs.root.contentWindow } - scrollTo (targetRow) { + /** + * @public + * @param {Number} targetRow + */ + scrollToRow (targetRow) { const blocks = this.getWindow().document.querySelectorAll( 'body>[data-line]' ) @@ -989,12 +1017,21 @@ export default class MarkdownPreview extends React.Component { const row = parseInt(block.getAttribute('data-line')) if (row > targetRow || index === blocks.length - 1) { block = blocks[index - 1] - block != null && this.getWindow().scrollTo(0, block.offsetTop) + block != null && this.scrollTo(0, block.offsetTop) break } } } + /** + * `document.body.scrollTo` + * @param {Number} x + * @param {Number} y + */ + scrollTo (x, y) { + this.getWindow().document.body.scrollTo(x, y) + } + preventImageDroppedHandler (e) { e.preventDefault() e.stopPropagation() @@ -1015,22 +1052,32 @@ export default class MarkdownPreview extends React.Component { e.preventDefault() e.stopPropagation() - const href = e.target.getAttribute('href') - const linkHash = href.split('/').pop() + const rawHref = e.target.getAttribute('href') + if (!rawHref) return // not checked href because parser will create file://... string for [empty link]() - if (!href) return + const parser = document.createElement('a') + parser.href = rawHref + const isStartWithHash = rawHref[0] === '#' + const { href, hash } = parser - const regexNoteInternalLink = /main.html#(.+)/ - if (regexNoteInternalLink.test(linkHash)) { - const targetId = mdurl.encode(linkHash.match(regexNoteInternalLink)[1]) - const targetElement = this.refs.root.contentWindow.document.getElementById( - targetId - ) + const linkHash = hash === '' ? rawHref : hash // needed because we're having special link formats that are removed by parser e.g. :line:10 - if (targetElement != null) { - this.getWindow().scrollTo(0, targetElement.offsetTop) + const extractIdRegex = /file:\/\/.*main.?\w*.html#/ // file://path/to/main(.development.)html + const regexNoteInternalLink = new RegExp(`${extractIdRegex.source}(.+)`) + if (isStartWithHash || regexNoteInternalLink.test(rawHref)) { + const posOfHash = linkHash.indexOf('#') + if (posOfHash > -1) { + const extractedId = linkHash.slice(posOfHash + 1) + const targetId = mdurl.encode(extractedId) + const targetElement = this.getWindow().document.getElementById( + targetId + ) + + if (targetElement != null) { + this.scrollTo(0, targetElement.offsetTop) + } + return } - return } // this will match the new uuid v4 hash and the old hash diff --git a/browser/components/MarkdownSplitEditor.js b/browser/components/MarkdownSplitEditor.js index 4477288a..f5996c59 100644 --- a/browser/components/MarkdownSplitEditor.js +++ b/browser/components/MarkdownSplitEditor.js @@ -78,24 +78,25 @@ class MarkdownSplitEditor extends React.Component { e.preventDefault() e.stopPropagation() const idMatch = /checkbox-([0-9]+)/ - const checkedMatch = /^\s*[\+\-\*] \[x\]/i - const uncheckedMatch = /^\s*[\+\-\*] \[ \]/ - const checkReplace = /\[x\]/i - const uncheckReplace = /\[ \]/ + const checkedMatch = /^(\s*>?)*\s*[+\-*] \[x]/i + const uncheckedMatch = /^(\s*>?)*\s*[+\-*] \[ ]/ + const checkReplace = /\[x]/i + const uncheckReplace = /\[ ]/ if (idMatch.test(e.target.getAttribute('id'))) { const lineIndex = parseInt(e.target.getAttribute('id').match(idMatch)[1], 10) - 1 const lines = this.refs.code.value .split('\n') const targetLine = lines[lineIndex] + let newLine = targetLine if (targetLine.match(checkedMatch)) { - lines[lineIndex] = targetLine.replace(checkReplace, '[ ]') + newLine = targetLine.replace(checkReplace, '[ ]') } if (targetLine.match(uncheckedMatch)) { - lines[lineIndex] = targetLine.replace(uncheckReplace, '[x]') + newLine = targetLine.replace(uncheckReplace, '[x]') } - this.refs.code.setValue(lines.join('\n')) + this.refs.code.setLineContent(lineIndex, newLine) } } @@ -150,7 +151,6 @@ class MarkdownSplitEditor extends React.Component { onMouseMove={e => this.handleMouseMove(e)} onMouseUp={e => this.handleMouseUp(e)}>
this.handleMouseDown(e)} >
( ) diff --git a/browser/components/NoteItem.js b/browser/components/NoteItem.js index 625bb38d..168af1ff 100644 --- a/browser/components/NoteItem.js +++ b/browser/components/NoteItem.js @@ -3,7 +3,7 @@ */ import PropTypes from 'prop-types' import React from 'react' -import { isArray } from 'lodash' +import { isArray, sortBy } from 'lodash' import invertColor from 'invert-color' import CSSModules from 'browser/lib/CSSModules' import { getTodoStatus } from 'browser/lib/getTodoStatus' @@ -43,7 +43,7 @@ const TagElementList = (tags, showTagsAlphabetically, coloredTags) => { } if (showTagsAlphabetically) { - return _.sortBy(tags).map(tag => TagElement({ tagName: tag, color: coloredTags[tag] })) + return sortBy(tags).map(tag => TagElement({ tagName: tag, color: coloredTags[tag] })) } else { return tags.map(tag => TagElement({ tagName: tag, color: coloredTags[tag] })) } @@ -148,15 +148,14 @@ NoteItem.propTypes = { tags: PropTypes.array, isStarred: PropTypes.bool.isRequired, isTrashed: PropTypes.bool.isRequired, - blog: { + blog: PropTypes.shape({ blogLink: PropTypes.string, blogId: PropTypes.number - } + }) }), handleNoteClick: PropTypes.func.isRequired, handleNoteContextMenu: PropTypes.func.isRequired, - handleDragStart: PropTypes.func.isRequired, - handleDragEnd: PropTypes.func.isRequired + handleDragStart: PropTypes.func.isRequired } export default CSSModules(NoteItem, styles) diff --git a/browser/components/SideNavFilter.js b/browser/components/SideNavFilter.js index 3a259ce7..5d5d627f 100644 --- a/browser/components/SideNavFilter.js +++ b/browser/components/SideNavFilter.js @@ -74,7 +74,7 @@ SideNavFilter.propTypes = { isStarredActive: PropTypes.bool.isRequired, isTrashedActive: PropTypes.bool.isRequired, handleStarredButtonClick: PropTypes.func.isRequired, - handleTrashdButtonClick: PropTypes.func.isRequired + handleTrashedButtonClick: PropTypes.func.isRequired } export default CSSModules(SideNavFilter, styles) diff --git a/browser/components/SnippetTab.js b/browser/components/SnippetTab.js index c030351f..d29130c7 100644 --- a/browser/components/SnippetTab.js +++ b/browser/components/SnippetTab.js @@ -114,7 +114,7 @@ class SnippetTab extends React.Component { > {snippet.name.trim().length > 0 ? snippet.name - : + : {i18n.__('Unnamed')} } diff --git a/browser/components/TodoProcess.js b/browser/components/TodoProcess.js index 251fd5b9..9d1f93cf 100644 --- a/browser/components/TodoProcess.js +++ b/browser/components/TodoProcess.js @@ -25,10 +25,10 @@ const TodoProcess = ({ ) TodoProcess.propTypes = { - todoStatus: { + todoStatus: PropTypes.exact({ total: PropTypes.number.isRequired, completed: PropTypes.number.isRequired - } + }) } export default CSSModules(TodoProcess, styles) diff --git a/browser/components/markdown.styl b/browser/components/markdown.styl index 4921b531..61829bc4 100644 --- a/browser/components/markdown.styl +++ b/browser/components/markdown.styl @@ -363,7 +363,10 @@ admonition_types = { danger: {color: #c2185b, icon: "block"}, caution: {color: #ffa726, icon: "warning"}, error: {color: #d32f2f, icon: "error_outline"}, - attention: {color: #455a64, icon: "priority_high"} + question: {color: #64dd17, icon: "help_outline"}, + quote: {color: #9e9e9e, icon: "format_quote"}, + abstract: {color: #00b0ff, icon: "subject"}, + attention: {color: #455a64, icon: "priority_high"}, } for name, val in admonition_types diff --git a/browser/components/render/MermaidRender.js b/browser/components/render/MermaidRender.js index e28e06ea..d9ea549b 100644 --- a/browser/components/render/MermaidRender.js +++ b/browser/components/render/MermaidRender.js @@ -19,7 +19,7 @@ function getId () { return id } -function render (element, content, theme) { +function render (element, content, theme, enableHTMLLabel) { try { const height = element.attributes.getNamedItem('data-height') if (height && height.value !== 'undefined') { @@ -29,7 +29,8 @@ function render (element, content, theme) { mermaidAPI.initialize({ theme: isDarkTheme ? 'dark' : 'default', themeCSS: isDarkTheme ? darkThemeStyling : '', - useMaxWidth: false + useMaxWidth: false, + flowchart: { htmlLabels: enableHTMLLabel } }) mermaidAPI.render(getId(), content, (svgGraph) => { element.innerHTML = svgGraph diff --git a/browser/lib/CSSModules.js b/browser/lib/CSSModules.js index 181274f4..691b44d2 100644 --- a/browser/lib/CSSModules.js +++ b/browser/lib/CSSModules.js @@ -1,5 +1,5 @@ import CSSModules from 'react-css-modules' export default function (component, styles) { - return CSSModules(component, styles, {errorWhenNotFound: false}) + return CSSModules(component, styles, {handleNotFoundStyleName: 'log'}) } diff --git a/browser/lib/Languages.js b/browser/lib/Languages.js index 8c3747a9..42755af9 100644 --- a/browser/lib/Languages.js +++ b/browser/lib/Languages.js @@ -11,6 +11,10 @@ const languages = [ name: 'Chinese (zh-TW)', locale: 'zh-TW' }, + { + name: 'Czech', + locale: 'cs' + }, { name: 'Danish', locale: 'da' @@ -62,10 +66,12 @@ const languages = [ { name: 'Spanish', locale: 'es-ES' - }, { + }, + { name: 'Turkish', locale: 'tr' - }, { + }, + { name: 'Thai', locale: 'th' } @@ -82,4 +88,3 @@ module.exports = { return languages } } - diff --git a/browser/lib/confirmDeleteNote.js b/browser/lib/confirmDeleteNote.js index 80d1ffc7..b67ed7e3 100644 --- a/browser/lib/confirmDeleteNote.js +++ b/browser/lib/confirmDeleteNote.js @@ -6,7 +6,7 @@ const { dialog } = remote export function confirmDeleteNote (confirmDeletion, permanent) { if (confirmDeletion || permanent) { const alertConfig = { - ype: 'warning', + type: 'warning', message: i18n.__('Confirm note deletion'), detail: i18n.__('This will permanently remove this note.'), buttons: [i18n.__('Confirm'), i18n.__('Cancel')] diff --git a/browser/lib/consts.js b/browser/lib/consts.js index 9c993055..ed497376 100644 --- a/browser/lib/consts.js +++ b/browser/lib/consts.js @@ -7,6 +7,7 @@ const CODEMIRROR_THEME_PATH = 'node_modules/codemirror/theme' const CODEMIRROR_EXTRA_THEME_PATH = 'extra_scripts/codemirror/theme' const isProduction = process.env.NODE_ENV === 'production' + const paths = [ isProduction ? path.join(app.getAppPath(), CODEMIRROR_THEME_PATH) : path.resolve(CODEMIRROR_THEME_PATH), isProduction ? path.join(app.getAppPath(), CODEMIRROR_EXTRA_THEME_PATH) : path.resolve(CODEMIRROR_EXTRA_THEME_PATH) @@ -18,7 +19,7 @@ const themes = paths return { name, - path: path.join(directory.split(/\//g).slice(-3).join('/'), file), + path: path.join(directory, file), className: `cm-s-${name}` } })) @@ -27,17 +28,16 @@ const themes = paths themes.splice(themes.findIndex(({ name }) => name === 'solarized'), 1, { name: 'solarized dark', - path: `${CODEMIRROR_THEME_PATH}/solarized.css`, + path: path.join(paths[0], 'solarized.css'), className: `cm-s-solarized cm-s-dark` }, { name: 'solarized light', - path: `${CODEMIRROR_THEME_PATH}/solarized.css`, + path: path.join(paths[0], 'solarized.css'), className: `cm-s-solarized cm-s-light` }) - themes.splice(0, 0, { name: 'default', - path: `${CODEMIRROR_THEME_PATH}/elegant.css`, + path: path.join(paths[0], 'elegant.css'), className: `cm-s-default` }) diff --git a/browser/lib/contextMenuBuilder.js b/browser/lib/contextMenuBuilder.js index cf92f52e..ff3349eb 100644 --- a/browser/lib/contextMenuBuilder.js +++ b/browser/lib/contextMenuBuilder.js @@ -1,6 +1,12 @@ +import i18n from 'browser/lib/i18n' +import fs from 'fs' + const {remote} = require('electron') const {Menu} = remote.require('electron') +const {clipboard} = remote.require('electron') +const {shell} = remote.require('electron') const spellcheck = require('./spellcheck') +const uri2path = require('file-uri-to-path') /** * Creates the context menu that is shown when there is a right click in the editor of a (not-snippet) note. @@ -62,4 +68,57 @@ const buildEditorContextMenu = function (editor, event) { return Menu.buildFromTemplate(template) } -module.exports = buildEditorContextMenu +/** + * Creates the context menu that is shown when there is a right click Markdown preview of a (not-snippet) note. + * @param {MarkdownPreview} markdownPreview + * @param {MouseEvent} event that has triggered the creation of the context menu + * @returns {Electron.Menu} The created electron context menu + */ +const buildMarkdownPreviewContextMenu = function (markdownPreview, event) { + if (markdownPreview == null || event == null || event.pageX == null || event.pageY == null) { + return null + } + + // Default context menu inclusions + const template = [{ + role: 'copy' + }, { + role: 'selectall' + }] + + if (event.target.tagName.toLowerCase() === 'a' && event.target.getAttribute('href')) { + // Link opener for files on the local system pointed to by href + const href = event.target.href + const isLocalFile = href.startsWith('file:') + if (isLocalFile) { + const absPath = uri2path(href) + try { + if (fs.lstatSync(absPath).isFile()) { + template.push( + { + label: i18n.__('Show in explorer'), + click: (e) => shell.showItemInFolder(absPath) + } + ) + } + } catch (e) { + console.log('Error while evaluating if the file is locally available', e) + } + } + + // Add option to context menu to copy url + template.push( + { + label: i18n.__('Copy Url'), + click: (e) => clipboard.writeText(href) + } + ) + } + return Menu.buildFromTemplate(template) +} + +module.exports = +{ + buildEditorContextMenu: buildEditorContextMenu, + buildMarkdownPreviewContextMenu: buildMarkdownPreviewContextMenu +} diff --git a/browser/lib/customMeta.js b/browser/lib/customMeta.js index 0d4ee1e3..b890cf55 100644 --- a/browser/lib/customMeta.js +++ b/browser/lib/customMeta.js @@ -1,5 +1,10 @@ import CodeMirror from 'codemirror' import 'codemirror-mode-elixir' -CodeMirror.modeInfo.push({name: 'Stylus', mime: 'text/x-styl', mode: 'stylus', ext: ['styl'], alias: ['styl']}) +const stylusCodeInfo = CodeMirror.modeInfo.find(info => info.name === 'Stylus') +if (stylusCodeInfo == null) { + CodeMirror.modeInfo.push({name: 'Stylus', mime: 'text/x-styl', mode: 'stylus', ext: ['styl'], alias: ['styl']}) +} else { + stylusCodeInfo.alias = ['styl'] +} CodeMirror.modeInfo.push({name: 'Elixir', mime: 'text/x-elixir', mode: 'elixir', ext: ['ex']}) diff --git a/browser/lib/getTodoStatus.js b/browser/lib/getTodoStatus.js index ab0d7809..8b552109 100644 --- a/browser/lib/getTodoStatus.js +++ b/browser/lib/getTodoStatus.js @@ -4,11 +4,11 @@ export function getTodoStatus (content) { let numberOfCompletedTodo = 0 splitted.forEach((line) => { - const trimmedLine = line.trim() - if (trimmedLine.match(/^[\+\-\*] \[(\s|x)\] ./i)) { + const trimmedLine = line.trim().replace(/^(>\s*)*/, '') + if (trimmedLine.match(/^[+\-*] \[(\s|x)] ./i)) { numberOfTodo++ } - if (trimmedLine.match(/^[\+\-\*] \[x\] ./i)) { + if (trimmedLine.match(/^[+\-*] \[x] ./i)) { numberOfCompletedTodo++ } }) diff --git a/browser/lib/keygen.js b/browser/lib/keygen.js index 814efedd..557a8a40 100644 --- a/browser/lib/keygen.js +++ b/browser/lib/keygen.js @@ -1,5 +1,4 @@ const crypto = require('crypto') -const _ = require('lodash') const uuidv4 = require('uuid/v4') module.exports = function (uuid) { diff --git a/browser/lib/markdown-it-sanitize-html.js b/browser/lib/markdown-it-sanitize-html.js index 8f6d86a8..641216e3 100644 --- a/browser/lib/markdown-it-sanitize-html.js +++ b/browser/lib/markdown-it-sanitize-html.js @@ -15,7 +15,7 @@ module.exports = function sanitizePlugin (md, options) { options ) } - if (state.tokens[tokenIdx].type === '_fence') { + if (state.tokens[tokenIdx].type.match(/.*_fence$/)) { // escapeHtmlCharacters has better performance state.tokens[tokenIdx].content = escapeHtmlCharacters( state.tokens[tokenIdx].content, @@ -96,6 +96,10 @@ function sanitizeInline (html, options) { function naughtyHRef (href, options) { // href = href.replace(/[\x00-\x20]+/g, '') + if (!href) { + // No href + return false + } href = href.replace(/<\!\-\-.*?\-\-\>/g, '') const matches = href.match(/^([a-zA-Z]+)\:/) diff --git a/browser/lib/markdown-toc-generator.js b/browser/lib/markdown-toc-generator.js index 8f027247..7c76c1f3 100644 --- a/browser/lib/markdown-toc-generator.js +++ b/browser/lib/markdown-toc-generator.js @@ -21,7 +21,7 @@ function uniqueSlug (slug, slugs, opts) { } function linkify (token) { - token.content = mdlink(token.content, '#' + token.slug) + token.content = mdlink(token.content, `#${decodeURI(token.slug)}`) return token } diff --git a/browser/lib/markdown.js b/browser/lib/markdown.js index df429b19..4174deae 100644 --- a/browser/lib/markdown.js +++ b/browser/lib/markdown.js @@ -2,7 +2,9 @@ import markdownit from 'markdown-it' import sanitize from './markdown-it-sanitize-html' import emoji from 'markdown-it-emoji' import math from '@rokt33r/markdown-it-math' +import mdurl from 'mdurl' import smartArrows from 'markdown-it-smartarrows' +import markdownItTocAndAnchor from '@hikerpig/markdown-it-toc-and-anchor' import _ from 'lodash' import ConfigManager from 'browser/main/lib/ConfigManager' import katex from 'katex' @@ -32,6 +34,7 @@ class Markdown { const updatedOptions = Object.assign(defaultOptions, options) this.md = markdownit(updatedOptions) + this.md.linkify.set({ fuzzyLink: false }) if (updatedOptions.sanitize !== 'NONE') { const allowedTags = ['iframe', 'input', 'b', @@ -121,10 +124,16 @@ class Markdown { slugify: require('./slugify') }) 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-admonition'), {types: ['note', 'hint', 'attention', 'caution', 'danger', 'error', 'quote', 'abstract', 'question']}) this.md.use(require('markdown-it-abbr')) this.md.use(require('markdown-it-sub')) this.md.use(require('markdown-it-sup')) + this.md.use(markdownItTocAndAnchor, { + toc: true, + tocPattern: /\[TOC\]/i, + anchorLink: false, + appendIdToHeading: false + }) this.md.use(require('./markdown-it-deflist')) this.md.use(require('./markdown-it-frontmatter')) @@ -149,9 +158,9 @@ class Markdown { const content = token.content.split('\n').slice(0, -1).map(line => { const match = /!\[[^\]]*]\(([^\)]*)\)/.exec(line) if (match) { - return match[1] + return mdurl.encode(match[1]) } else { - return line + return mdurl.encode(line) } }).join('\n') @@ -181,32 +190,47 @@ class Markdown { }) const deflate = require('markdown-it-plantuml/lib/deflate') - this.md.use(require('markdown-it-plantuml'), { - generateSource: function (umlCode) { - const stripTrailingSlash = (url) => url.endsWith('/') ? url.slice(0, -1) : url - const serverAddress = stripTrailingSlash(config.preview.plantUMLServerAddress) + '/svg' - const s = unescape(encodeURIComponent(umlCode)) - const zippedCode = deflate.encode64( - deflate.zip_deflate(`@startuml\n${s}\n@enduml`, 9) - ) - return `${serverAddress}/${zippedCode}` - } + const plantuml = require('markdown-it-plantuml') + const plantUmlStripTrailingSlash = (url) => url.endsWith('/') ? url.slice(0, -1) : url + const plantUmlServerAddress = plantUmlStripTrailingSlash(config.preview.plantUMLServerAddress) + const parsePlantUml = function (umlCode, openMarker, closeMarker, type) { + const s = unescape(encodeURIComponent(umlCode)) + const zippedCode = deflate.encode64( + deflate.zip_deflate(`${openMarker}\n${s}\n${closeMarker}`, 9) + ) + return `${plantUmlServerAddress}/${type}/${zippedCode}` + } + + this.md.use(plantuml, { + generateSource: (umlCode) => parsePlantUml(umlCode, '@startuml', '@enduml', 'svg') }) - // Ditaa support - this.md.use(require('markdown-it-plantuml'), { + // Ditaa support. PlantUML server doesn't support Ditaa in SVG, so we set the format as PNG at the moment. + this.md.use(plantuml, { openMarker: '@startditaa', closeMarker: '@endditaa', - generateSource: function (umlCode) { - const stripTrailingSlash = (url) => url.endsWith('/') ? url.slice(0, -1) : url - // Currently PlantUML server doesn't support Ditaa in SVG, so we set the format as PNG at the moment. - const serverAddress = stripTrailingSlash(config.preview.plantUMLServerAddress) + '/png' - const s = unescape(encodeURIComponent(umlCode)) - const zippedCode = deflate.encode64( - deflate.zip_deflate(`@startditaa\n${s}\n@endditaa`, 9) - ) - return `${serverAddress}/${zippedCode}` - } + generateSource: (umlCode) => parsePlantUml(umlCode, '@startditaa', '@endditaa', 'png') + }) + + // Mindmap support + this.md.use(plantuml, { + openMarker: '@startmindmap', + closeMarker: '@endmindmap', + generateSource: (umlCode) => parsePlantUml(umlCode, '@startmindmap', '@endmindmap', 'svg') + }) + + // WBS support + this.md.use(plantuml, { + openMarker: '@startwbs', + closeMarker: '@endwbs', + generateSource: (umlCode) => parsePlantUml(umlCode, '@startwbs', '@endwbs', 'svg') + }) + + // Gantt support + this.md.use(plantuml, { + openMarker: '@startgantt', + closeMarker: '@endgantt', + generateSource: (umlCode) => parsePlantUml(umlCode, '@startgantt', '@endgantt', 'svg') }) // Override task item @@ -287,7 +311,9 @@ class Markdown { case 'list_item_open': case 'paragraph_open': case 'table_open': - token.attrPush(['data-line', token.map[0]]) + if (token.map) { + token.attrPush(['data-line', token.map[0]]) + } } }) const result = originalRender.call(this.md.renderer, tokens, options, env) diff --git a/browser/lib/newNote.js b/browser/lib/newNote.js index d8ef196f..4eec24f1 100644 --- a/browser/lib/newNote.js +++ b/browser/lib/newNote.js @@ -1,7 +1,8 @@ -import { hashHistory } from 'react-router' import dataApi from 'browser/main/lib/dataApi' import ee from 'browser/main/lib/eventEmitter' import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig' +import queryString from 'query-string' +import { push } from 'connected-react-router' export function createMarkdownNote (storage, folder, dispatch, location, params, config) { AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_MARKDOWN') @@ -28,10 +29,10 @@ export function createMarkdownNote (storage, folder, dispatch, location, params, note: note }) - hashHistory.push({ + dispatch(push({ pathname: location.pathname, - query: { key: noteHash } - }) + search: queryString.stringify({ key: noteHash }) + })) ee.emit('list:jump', noteHash) ee.emit('detail:focus') }) @@ -70,10 +71,10 @@ export function createSnippetNote (storage, folder, dispatch, location, params, type: 'UPDATE_NOTE', note: note }) - hashHistory.push({ + dispatch(push({ pathname: location.pathname, - query: { key: noteHash } - }) + search: queryString.stringify({ key: noteHash }) + })) ee.emit('list:jump', noteHash) ee.emit('detail:focus') }) diff --git a/browser/lib/slugify.js b/browser/lib/slugify.js index a3447a90..21c18e02 100644 --- a/browser/lib/slugify.js +++ b/browser/lib/slugify.js @@ -1,17 +1,11 @@ -import diacritics from 'diacritics-map' - -function replaceDiacritics (str) { - return str.replace(/[À-ž]/g, function (ch) { - return diacritics[ch] || ch - }) -} - module.exports = function slugify (title) { - let slug = title.trim() + const slug = encodeURI( + title.trim() + .replace(/^\s+/, '') + .replace(/\s+$/, '') + .replace(/\s+/g, '-') + .replace(/[\]\[\!\'\#\$\%\&\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\{\|\}\~\`]/g, '') + ) - slug = replaceDiacritics(slug) - - slug = slug.replace(/[^\w\s-]/g, '').replace(/\s+/g, '-') - - return encodeURI(slug).replace(/\-+$/, '') + return slug } diff --git a/browser/lib/turndown.js b/browser/lib/turndown.js new file mode 100644 index 00000000..a1c3e128 --- /dev/null +++ b/browser/lib/turndown.js @@ -0,0 +1,9 @@ +const TurndownService = require('turndown') +const { gfm } = require('turndown-plugin-gfm') + +export const createTurndownService = function () { + const turndown = new TurndownService() + turndown.use(gfm) + turndown.remove('script') + return turndown +} diff --git a/browser/lib/utils.js b/browser/lib/utils.js index 4bcc9698..9f6f1425 100644 --- a/browser/lib/utils.js +++ b/browser/lib/utils.js @@ -136,9 +136,24 @@ export function isMarkdownTitleURL (str) { return /(^#{1,6}\s)(?:\w+:|^)\/\/(?:[^\s\.]+\.\S{2}|localhost[\:?\d]*)/.test(str) } +export function humanFileSize (bytes) { + const threshold = 1000 + if (Math.abs(bytes) < threshold) { + return bytes + ' B' + } + var units = ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] + var u = -1 + do { + bytes /= threshold + ++u + } while (Math.abs(bytes) >= threshold && u < units.length - 1) + return bytes.toFixed(1) + ' ' + units[u] +} + export default { lastFindInArray, escapeHtmlCharacters, isObjectEqual, - isMarkdownTitleURL + isMarkdownTitleURL, + humanFileSize } diff --git a/browser/main/Detail/FromUrlButton.js b/browser/main/Detail/FromUrlButton.js new file mode 100644 index 00000000..0d1c1b4c --- /dev/null +++ b/browser/main/Detail/FromUrlButton.js @@ -0,0 +1,69 @@ +import PropTypes from 'prop-types' +import React from 'react' +import CSSModules from 'browser/lib/CSSModules' +import styles from './FromUrlButton.styl' +import _ from 'lodash' +import i18n from 'browser/lib/i18n' + +class FromUrlButton extends React.Component { + constructor (props) { + super(props) + + this.state = { + isActive: false + } + } + + handleMouseDown (e) { + this.setState({ + isActive: true + }) + } + + handleMouseUp (e) { + this.setState({ + isActive: false + }) + } + + handleMouseLeave (e) { + this.setState({ + isActive: false + }) + } + + render () { + const { className } = this.props + + return ( + + ) + } +} + +FromUrlButton.propTypes = { + isActive: PropTypes.bool, + onClick: PropTypes.func, + className: PropTypes.string +} + +export default CSSModules(FromUrlButton, styles) diff --git a/browser/main/Detail/FromUrlButton.styl b/browser/main/Detail/FromUrlButton.styl new file mode 100644 index 00000000..66c2d730 --- /dev/null +++ b/browser/main/Detail/FromUrlButton.styl @@ -0,0 +1,41 @@ +.root + top 45px + topBarButtonRight() + &:hover + transition 0.2s + color alpha($ui-favorite-star-button-color, 0.6) + &:hover .tooltip + opacity 1 + +.tooltip + tooltip() + position absolute + pointer-events none + top 50px + right 125px + width 90px + z-index 200 + padding 5px + line-height normal + border-radius 2px + opacity 0 + transition 0.1s + +.root--active + @extend .root + transition 0.15s + color $ui-favorite-star-button-color + &:hover + transition 0.2s + color alpha($ui-favorite-star-button-color, 0.6) + +.icon + transition transform 0.15s + height 13px + +body[data-theme="dark"] + .root + topBarButtonDark() + &:hover + transition 0.2s + color alpha($ui-favorite-star-button-color, 0.6) diff --git a/browser/main/Detail/FullscreenButton.js b/browser/main/Detail/FullscreenButton.js index bd76447c..eb33165f 100644 --- a/browser/main/Detail/FullscreenButton.js +++ b/browser/main/Detail/FullscreenButton.js @@ -11,7 +11,7 @@ const FullscreenButton = ({ const hotkey = (OSX ? i18n.__('Command(⌘)') : i18n.__('Ctrl(^)')) + '+B' return ( ) diff --git a/browser/main/Detail/InfoPanel.js b/browser/main/Detail/InfoPanel.js index 8fe0a855..86b5ae86 100644 --- a/browser/main/Detail/InfoPanel.js +++ b/browser/main/Detail/InfoPanel.js @@ -60,7 +60,7 @@ class InfoPanel extends React.Component {
- { e.target.select() }} /> + { e.target.select() }} /> diff --git a/browser/main/Detail/MarkdownNoteDetail.js b/browser/main/Detail/MarkdownNoteDetail.js index cf3be072..207e1e2b 100755 --- a/browser/main/Detail/MarkdownNoteDetail.js +++ b/browser/main/Detail/MarkdownNoteDetail.js @@ -9,7 +9,6 @@ import StarButton from './StarButton' import TagSelect from './TagSelect' import FolderSelect from './FolderSelect' import dataApi from 'browser/main/lib/dataApi' -import { hashHistory } from 'react-router' import ee from 'browser/main/lib/eventEmitter' import markdown from 'browser/lib/markdownTextHelper' import StatusBar from '../StatusBar' @@ -30,6 +29,8 @@ import { getTodoPercentageOfCompleted } from 'browser/lib/getTodoStatus' import striptags from 'striptags' import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote' import markdownToc from 'browser/lib/markdown-toc-generator' +import queryString from 'query-string' +import { replace } from 'connected-react-router' class MarkdownNoteDetail extends React.Component { constructor (props) { @@ -66,9 +67,6 @@ class MarkdownNoteDetail extends React.Component { }) ee.on('hotkey:deletenote', this.handleDeleteNote.bind(this)) ee.on('code:generate-toc', this.generateToc) - - // Focus content if using blur or double click - if (this.state.switchPreview === 'BLUR' || this.state.switchPreview === 'DBL_CLICK') this.focus() } componentWillReceiveProps (nextProps) { @@ -83,6 +81,20 @@ class MarkdownNoteDetail extends React.Component { if (this.refs.tags) this.refs.tags.reset() }) } + + // Focus content if using blur or double click + // --> Moved here from componentDidMount so a re-render during search won't set focus to the editor + const {switchPreview} = nextProps.config.editor + + if (this.state.switchPreview !== switchPreview) { + this.setState({ + switchPreview + }) + if (switchPreview === 'BLUR' || switchPreview === 'DBL_CLICK') { + console.log('setting focus', switchPreview) + this.focus() + } + } } componentWillUnmount () { @@ -159,12 +171,12 @@ class MarkdownNoteDetail extends React.Component { originNote: note, note: newNote }) - hashHistory.replace({ + dispatch(replace({ pathname: location.pathname, - query: { + search: queryString.stringify({ key: newNote.key - } - }) + }) + })) this.setState({ isMovingNote: false }) @@ -298,7 +310,7 @@ class MarkdownNoteDetail extends React.Component { } getToggleLockButton () { - return this.state.isLocked ? '../resources/icon/icon-previewoff-on.svg' : '../resources/icon/icon-previewoff-off.svg' + return this.state.isLocked ? '../resources/icon/icon-lock.svg' : '../resources/icon/icon-unlock.svg' } handleDeleteKeyDown (e) { @@ -397,7 +409,7 @@ class MarkdownNoteDetail extends React.Component { } render () { - const { data, location, config } = this.props + const { data, dispatch, location, config } = this.props const { note, editorType } = this.state const storageKey = note.storage const folderKey = note.folder @@ -437,7 +449,7 @@ class MarkdownNoteDetail extends React.Component { const detailTopBar =
-
+
@@ -459,6 +472,7 @@ class MarkdownNoteDetail extends React.Component {
this.handleSwitchMode(e)} editorType={editorType} /> + this.handleStarButtonClick(e)} isActive={note.isStarred} @@ -471,7 +485,7 @@ class MarkdownNoteDetail extends React.Component { onFocus={(e) => this.handleFocus(e)} onMouseDown={(e) => this.handleLockButtonMouseDown(e)} > - + {this.state.isLocked ? Unlock : Lock} @@ -491,14 +505,14 @@ class MarkdownNoteDetail extends React.Component { div + > button + -webkit-user-drag none + user-select none + > img, span + -webkit-user-drag none + user-select none \ No newline at end of file diff --git a/browser/main/Detail/PermanentDeleteButton.js b/browser/main/Detail/PermanentDeleteButton.js index fa00ef17..7c27ede1 100644 --- a/browser/main/Detail/PermanentDeleteButton.js +++ b/browser/main/Detail/PermanentDeleteButton.js @@ -10,7 +10,7 @@ const PermanentDeleteButton = ({ ) diff --git a/browser/main/Detail/SnippetNoteDetail.js b/browser/main/Detail/SnippetNoteDetail.js index 80378793..ec9a1d0b 100644 --- a/browser/main/Detail/SnippetNoteDetail.js +++ b/browser/main/Detail/SnippetNoteDetail.js @@ -8,7 +8,6 @@ import StarButton from './StarButton' import TagSelect from './TagSelect' import FolderSelect from './FolderSelect' import dataApi from 'browser/main/lib/dataApi' -import {hashHistory} from 'react-router' import ee from 'browser/main/lib/eventEmitter' import CodeMirror from 'codemirror' import 'codemirror-mode-elixir' @@ -18,7 +17,6 @@ import context from 'browser/lib/context' import ConfigManager from 'browser/main/lib/ConfigManager' import _ from 'lodash' import {findNoteTitle} from 'browser/lib/findNoteTitle' -import convertModeName from 'browser/lib/convertModeName' import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig' import FullscreenButton from './FullscreenButton' import TrashButton from './TrashButton' @@ -31,6 +29,8 @@ import { formatDate } from 'browser/lib/date-formatter' import i18n from 'browser/lib/i18n' import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote' import markdownToc from 'browser/lib/markdown-toc-generator' +import queryString from 'query-string' +import { replace } from 'connected-react-router' const electron = require('electron') const { remote } = electron @@ -166,12 +166,12 @@ class SnippetNoteDetail extends React.Component { originNote: note, note: newNote }) - hashHistory.replace({ + dispatch(replace({ pathname: location.pathname, - query: { + search: queryString.stringify({ key: newNote.key - } - }) + }) + })) this.setState({ isMovingNote: false }) @@ -518,6 +518,19 @@ class SnippetNoteDetail extends React.Component { ]) } + handleWrapLineButtonClick (e) { + context.popup([ + { + label: 'on', + click: (e) => this.handleWrapLineItemClick(e, true) + }, + { + label: 'off', + click: (e) => this.handleWrapLineItemClick(e, false) + } + ]) + } + handleIndentSizeItemClick (e, indentSize) { const { config, dispatch } = this.props const editor = Object.assign({}, config.editor, { @@ -550,6 +563,22 @@ class SnippetNoteDetail extends React.Component { }) } + handleWrapLineItemClick (e, lineWrapping) { + const { config, dispatch } = this.props + const editor = Object.assign({}, config.editor, { + lineWrapping + }) + ConfigManager.set({ + editor + }) + dispatch({ + type: 'SET_CONFIG', + config: { + editor + } + }) + } + focus () { this.refs.description.focus() } @@ -670,7 +699,7 @@ class SnippetNoteDetail extends React.Component { } render () { - const { data, config, location } = this.props + const { data, dispatch, config, location } = this.props const { note } = this.state const storageKey = note.storage @@ -720,6 +749,7 @@ class SnippetNoteDetail extends React.Component { mode={snippet.mode || (autoDetect ? null : config.editor.snippetDefaultLanguage)} value={snippet.content} linesHighlighted={snippet.linesHighlighted} + lineWrapping={config.editor.lineWrapping} theme={config.editor.theme} fontFamily={config.editor.fontFamily} fontSize={editorFontSize} @@ -778,7 +808,7 @@ class SnippetNoteDetail extends React.Component { const detailTopBar =
-
+
this.handleChange(e)} coloredTags={config.coloredTags} /> @@ -814,7 +845,7 @@ class SnippetNoteDetail extends React.Component { +
(
-
onClick('SPLIT')}> - +
onClick('SPLIT')}> +
-
onClick('EDITOR_PREVIEW')}> - +
onClick('EDITOR_PREVIEW')}> +
{i18n.__('Toggle Mode')}
@@ -20,7 +20,7 @@ const ToggleModeButton = ({ ToggleModeButton.propTypes = { onClick: PropTypes.func.isRequired, - editorType: PropTypes.string.Required + editorType: PropTypes.string.isRequired } export default CSSModules(ToggleModeButton, styles) diff --git a/browser/main/Detail/ToggleModeButton.styl b/browser/main/Detail/ToggleModeButton.styl index 2b47b932..39d30973 100644 --- a/browser/main/Detail/ToggleModeButton.styl +++ b/browser/main/Detail/ToggleModeButton.styl @@ -75,3 +75,10 @@ body[data-theme="dracula"] .active background-color #bd93f9 box-shadow 2px 0px 7px #222222 + +.control-toggleModeButton + -webkit-user-drag none + user-select none + > div img + -webkit-user-drag none + user-select none diff --git a/browser/main/Detail/TrashButton.js b/browser/main/Detail/TrashButton.js index d26be66e..8ca27ce9 100644 --- a/browser/main/Detail/TrashButton.js +++ b/browser/main/Detail/TrashButton.js @@ -10,7 +10,7 @@ const TrashButton = ({ ) diff --git a/browser/main/Detail/index.js b/browser/main/Detail/index.js index b6b6ef14..95b9d73d 100644 --- a/browser/main/Detail/index.js +++ b/browser/main/Detail/index.js @@ -10,6 +10,7 @@ import StatusBar from '../StatusBar' import i18n from 'browser/lib/i18n' import debounceRender from 'react-debounce-render' import searchFromNotes from 'browser/lib/search' +import queryString from 'query-string' const OSX = global.process.platform === 'darwin' @@ -36,11 +37,11 @@ class Detail extends React.Component { } render () { - const { location, data, params, config } = this.props + const { location, data, match: { params }, config } = this.props + const noteKey = location.search !== '' && queryString.parse(location.search).key let note = null - if (location.query.key != null) { - const noteKey = location.query.key + if (location.search !== '') { const allNotes = data.noteMap.map(note => note) const trashedNotes = data.trashedSet.toJS().map(uniqueKey => data.noteMap.get(uniqueKey)) let displayedNotes = allNotes @@ -49,16 +50,14 @@ class Detail extends React.Component { const searchStr = params.searchword displayedNotes = searchStr === undefined || searchStr === '' ? allNotes : searchFromNotes(allNotes, searchStr) - } - - if (location.pathname.match(/\/tags/)) { + } else if (location.pathname.match(/^\/tags/)) { const listOfTags = params.tagname.split(' ') displayedNotes = data.noteMap.map(note => note).filter(note => listOfTags.every(tag => note.tags.includes(tag)) ) } - if (location.pathname.match(/\/trashed/)) { + if (location.pathname.match(/^\/trashed/)) { displayedNotes = trashedNotes } else { displayedNotes = _.differenceWith(displayedNotes, trashedNotes, (note, trashed) => note.key === trashed.key) diff --git a/browser/main/DevTools/index.dev.js b/browser/main/DevTools/index.dev.js new file mode 100644 index 00000000..77d3eccd --- /dev/null +++ b/browser/main/DevTools/index.dev.js @@ -0,0 +1,16 @@ +import React from 'react' +import { createDevTools } from 'redux-devtools' +import LogMonitor from 'redux-devtools-log-monitor' +import DockMonitor from 'redux-devtools-dock-monitor' + +const DevTools = createDevTools( + + + +) + +export default DevTools diff --git a/browser/main/DevTools/index.js b/browser/main/DevTools/index.js new file mode 100644 index 00000000..d39d5fbb --- /dev/null +++ b/browser/main/DevTools/index.js @@ -0,0 +1,8 @@ +/* eslint-disable no-undef */ +if (process.env.NODE_ENV === 'development') { + // eslint-disable-next-line global-require + module.exports = require('./index.dev').default +} else { + // eslint-disable-next-line global-require + module.exports = require('./index.prod').default +} diff --git a/browser/main/DevTools/index.prod.js b/browser/main/DevTools/index.prod.js new file mode 100644 index 00000000..762cae2c --- /dev/null +++ b/browser/main/DevTools/index.prod.js @@ -0,0 +1,6 @@ +import React from 'react' + +const DevTools = () =>
+DevTools.instrument = () => {} + +export default DevTools diff --git a/browser/main/Main.js b/browser/main/Main.js index 26fc8377..e277c421 100644 --- a/browser/main/Main.js +++ b/browser/main/Main.js @@ -12,11 +12,11 @@ import _ from 'lodash' import ConfigManager from 'browser/main/lib/ConfigManager' import mobileAnalytics from 'browser/main/lib/AwsMobileAnalyticsConfig' import eventEmitter from 'browser/main/lib/eventEmitter' -import { hashHistory } from 'react-router' -import store from 'browser/main/store' +import { store } from 'browser/main/store' import i18n from 'browser/lib/i18n' import { getLocales } from 'browser/lib/Languages' import applyShortcuts from 'browser/main/lib/shortcutManager' +import { push } from 'connected-react-router' const path = require('path') const electron = require('electron') const { remote } = electron @@ -102,7 +102,7 @@ class Main extends React.Component { { name: 'example.js', mode: 'javascript', - content: "var boostnote = document.getElementById('enjoy').innerHTML\n\nconsole.log(boostnote)", + content: "var boostnote = document.getElementById('hello').innerHTML\n\nconsole.log(boostnote)", linesHighlighted: [] } ] @@ -132,7 +132,7 @@ class Main extends React.Component { .then(() => data.storage) }) .then(storage => { - hashHistory.push('/storages/' + storage.key) + store.dispatch(push('/storages/' + storage.key)) }) .catch(err => { throw err @@ -169,6 +169,7 @@ class Main extends React.Component { } }) + // eslint-disable-next-line no-undef delete CodeMirror.keyMap.emacs['Ctrl-V'] eventEmitter.on('editor:fullscreen', this.toggleFullScreen) @@ -311,7 +312,7 @@ class Main extends React.Component { onMouseUp={e => this.handleMouseUp(e)} > {!config.isSideNavFolded && @@ -341,7 +342,7 @@ class Main extends React.Component { 'dispatch', 'config', 'data', - 'params', + 'match', 'location' ])} /> @@ -351,7 +352,7 @@ class Main extends React.Component { 'dispatch', 'data', 'config', - 'params', + 'match', 'location' ])} /> @@ -373,7 +374,7 @@ class Main extends React.Component { 'dispatch', 'data', 'config', - 'params', + 'match', 'location' ])} ignorePreviewPointerEvents={this.state.isRightSliderFocused} diff --git a/browser/main/NewNoteButton/index.js b/browser/main/NewNoteButton/index.js index c34443be..27e2baa5 100644 --- a/browser/main/NewNoteButton/index.js +++ b/browser/main/NewNoteButton/index.js @@ -21,23 +21,20 @@ class NewNoteButton extends React.Component { this.state = { } - this.newNoteHandler = () => { - this.handleNewNoteButtonClick() - } + this.handleNewNoteButtonClick = this.handleNewNoteButtonClick.bind(this) } componentDidMount () { - eventEmitter.on('top:new-note', this.newNoteHandler) + eventEmitter.on('top:new-note', this.handleNewNoteButtonClick) } componentWillUnmount () { - eventEmitter.off('top:new-note', this.newNoteHandler) + eventEmitter.off('top:new-note', this.handleNewNoteButtonClick) } handleNewNoteButtonClick (e) { - const { location, params, dispatch, config } = this.props + const { location, dispatch, match: { params }, config } = this.props const { storage, folder } = this.resolveTargetFolder() - if (config.ui.defaultNote === 'MARKDOWN_NOTE') { createMarkdownNote(storage.key, folder.key, dispatch, location, params, config) } else if (config.ui.defaultNote === 'SNIPPET_NOTE') { @@ -55,9 +52,8 @@ class NewNoteButton extends React.Component { } resolveTargetFolder () { - const { data, params } = this.props + const { data, match: { params } } = this.props let storage = data.storageMap.get(params.storageKey) - // Find first storage if (storage == null) { for (const kv of data.storageMap) { @@ -93,8 +89,8 @@ class NewNoteButton extends React.Component { >
@@ -1155,6 +1163,7 @@ class NoteList extends React.Component { tabIndex='-1' onKeyDown={(e) => this.handleNoteListKeyDown(e)} onKeyUp={this.handleNoteListKeyUp} + onBlur={this.handleNoteListBlur} > {noteList}
diff --git a/browser/main/SideNav/PreferenceButton.js b/browser/main/SideNav/PreferenceButton.js index 187171f4..187bc41a 100644 --- a/browser/main/SideNav/PreferenceButton.js +++ b/browser/main/SideNav/PreferenceButton.js @@ -8,7 +8,7 @@ const PreferenceButton = ({ onClick }) => ( ) diff --git a/browser/main/SideNav/StorageItem.js b/browser/main/SideNav/StorageItem.js index e336f3ce..5cd4a491 100644 --- a/browser/main/SideNav/StorageItem.js +++ b/browser/main/SideNav/StorageItem.js @@ -2,7 +2,6 @@ import PropTypes from 'prop-types' import React from 'react' import CSSModules from 'browser/lib/CSSModules' import styles from './StorageItem.styl' -import { hashHistory } from 'react-router' import modal from 'browser/main/lib/modal' import CreateFolderModal from 'browser/main/modals/CreateFolderModal' import RenameFolderModal from 'browser/main/modals/RenameFolderModal' @@ -12,6 +11,7 @@ import _ from 'lodash' import { SortableElement } from 'react-sortable-hoc' import i18n from 'browser/lib/i18n' import context from 'browser/lib/context' +import { push } from 'connected-react-router' const { remote } = require('electron') const { dialog } = remote @@ -134,14 +134,14 @@ class StorageItem extends React.Component { } handleHeaderInfoClick (e) { - const { storage } = this.props - hashHistory.push('/storages/' + storage.key) + const { storage, dispatch } = this.props + dispatch(push('/storages/' + storage.key)) } handleFolderButtonClick (folderKey) { return (e) => { - const { storage } = this.props - hashHistory.push('/storages/' + storage.key + '/folders/' + folderKey) + const { storage, dispatch } = this.props + dispatch(push('/storages/' + storage.key + '/folders/' + folderKey)) } } @@ -362,14 +362,14 @@ class StorageItem extends React.Component { }
{this.state.isOpen && -
+
{folderList}
} diff --git a/browser/main/SideNav/index.js b/browser/main/SideNav/index.js index 65949860..331e959c 100644 --- a/browser/main/SideNav/index.js +++ b/browser/main/SideNav/index.js @@ -1,5 +1,6 @@ import PropTypes from 'prop-types' import React from 'react' +import { push } from 'connected-react-router' import CSSModules from 'browser/lib/CSSModules' import dataApi from 'browser/main/lib/dataApi' import styles from './SideNav.styl' @@ -22,9 +23,10 @@ import context from 'browser/lib/context' import { remote } from 'electron' import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote' import ColorPicker from 'browser/components/ColorPicker' +import { every, sortBy } from 'lodash' function matchActiveTags (tags, activeTags) { - return _.every(activeTags, v => tags.indexOf(v) >= 0) + return every(activeTags, v => tags.indexOf(v) >= 0) } class SideNav extends React.Component { @@ -61,14 +63,14 @@ class SideNav extends React.Component { deleteTag (tag) { const selectedButton = remote.dialog.showMessageBox(remote.getCurrentWindow(), { - ype: 'warning', + type: '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 { data, dispatch, location, match: { params } } = this.props const notes = data.noteMap .map(note => note) @@ -98,7 +100,7 @@ class SideNav extends React.Component { if (index !== -1) { tags.splice(index, 1) - this.context.router.push(`/tags/${tags.map(tag => encodeURIComponent(tag)).join(' ')}`) + dispatch(push(`/tags/${tags.map(tag => encodeURIComponent(tag)).join(' ')}`)) } } }) @@ -130,13 +132,13 @@ class SideNav extends React.Component { } handleHomeButtonClick (e) { - const { router } = this.context - router.push('/home') + const { dispatch } = this.props + dispatch(push('/home')) } handleStarredButtonClick (e) { - const { router } = this.context - router.push('/starred') + const { dispatch } = this.props + dispatch(push('/starred')) } handleTagContextMenu (e, tag) { @@ -223,18 +225,18 @@ class SideNav extends React.Component { } handleTrashedButtonClick (e) { - const { router } = this.context - router.push('/trashed') + const { dispatch } = this.props + dispatch(push('/trashed')) } handleSwitchFoldersButtonClick () { - const { router } = this.context - router.push('/home') + const { dispatch } = this.props + dispatch(push('/home')) } handleSwitchTagsButtonClick () { - const { router } = this.context - router.push('/alltags') + const { dispatch } = this.props + dispatch(push('/alltags')) } onSortEnd (storage) { @@ -326,6 +328,7 @@ class SideNav extends React.Component {
{this.tagListComponent(data)}
+
) } @@ -338,7 +341,7 @@ class SideNav extends React.Component { const { colorPicker, showSearch, searchText } = this.state const activeTags = this.getActiveTags(location.pathname) const relatedTags = this.getRelatedTags(activeTags, data.noteMap) - let tagList = _.sortBy(data.tagNoteMap.map( + let tagList = sortBy(data.tagNoteMap.map( (tag, name) => ({ name, size: tag.size, related: relatedTags.has(name) }) ).filter( tag => tag.size > 0 @@ -354,7 +357,7 @@ class SideNav extends React.Component { }) } if (config.sortTagsBy === 'COUNTER') { - tagList = _.sortBy(tagList, item => (0 - item.size)) + tagList = sortBy(tagList, item => (0 - item.size)) } if (config.ui.showOnlyRelatedTags && (relatedTags.size > 0)) { tagList = tagList.filter( @@ -407,8 +410,8 @@ class SideNav extends React.Component { } handleClickTagListItem (name) { - const { router } = this.context - router.push(`/tags/${encodeURIComponent(name)}`) + const { dispatch } = this.props + dispatch(push(`/tags/${encodeURIComponent(name)}`)) } handleSortTagsByChange (e) { @@ -426,8 +429,7 @@ class SideNav extends React.Component { } handleClickNarrowToTag (tag) { - const { router } = this.context - const { location } = this.props + const { dispatch, location } = this.props const listOfTags = this.getActiveTags(location.pathname) const indexOfTag = listOfTags.indexOf(tag) if (indexOfTag > -1) { @@ -435,7 +437,7 @@ class SideNav extends React.Component { } else { listOfTags.push(tag) } - router.push(`/tags/${encodeURIComponent(listOfTags.join(' '))}`) + dispatch(push(`/tags/${encodeURIComponent(listOfTags.join(' '))}`)) } emptyTrash (entries) { @@ -484,7 +486,7 @@ class SideNav extends React.Component { const isFolded = config.isSideNavFolded const style = {} if (!isFolded) style.width = this.props.width - const isTagActive = location.pathname.match(/tag/) + const isTagActive = /tag/.test(location.pathname) const navSearch = (
diff --git a/browser/main/TopBar/index.js b/browser/main/TopBar/index.js index 91256daf..09fd56b2 100644 --- a/browser/main/TopBar/index.js +++ b/browser/main/TopBar/index.js @@ -7,6 +7,8 @@ import ee from 'browser/main/lib/eventEmitter' import NewNoteButton from 'browser/main/NewNoteButton' import i18n from 'browser/lib/i18n' import debounce from 'lodash/debounce' +import CInput from 'react-composition-input' +import { push } from 'connected-react-router' class TopBar extends React.Component { constructor (props) { @@ -15,26 +17,36 @@ class TopBar extends React.Component { this.state = { search: '', searchOptions: [], - isSearching: false, - isAlphabet: false, - isIME: false, - isConfirmTranslation: false + isSearching: false } + const { dispatch } = this.props + this.focusSearchHandler = () => { this.handleOnSearchFocus() } this.codeInitHandler = this.handleCodeInit.bind(this) + this.handleKeyDown = this.handleKeyDown.bind(this) + this.handleSearchFocus = this.handleSearchFocus.bind(this) + this.handleSearchBlur = this.handleSearchBlur.bind(this) + this.handleSearchChange = this.handleSearchChange.bind(this) + this.handleSearchClearButton = this.handleSearchClearButton.bind(this) - this.updateKeyword = debounce(this.updateKeyword, 1000 / 60, { + this.debouncedUpdateKeyword = debounce((keyword) => { + dispatch(push(`/searched/${encodeURIComponent(keyword)}`)) + this.setState({ + search: keyword + }) + ee.emit('top:search', keyword) + }, 1000 / 60, { maxWait: 1000 / 8 }) } componentDidMount () { - const { params } = this.props - const searchWord = params.searchword + const { match: { params } } = this.props + const searchWord = params && params.searchword if (searchWord !== undefined) { this.setState({ search: searchWord, @@ -51,22 +63,22 @@ class TopBar extends React.Component { } handleSearchClearButton (e) { - const { router } = this.context + const { dispatch } = this.props this.setState({ search: '', isSearching: false }) this.refs.search.childNodes[0].blur - router.push('/searched') + dispatch(push('/searched')) e.preventDefault() + this.debouncedUpdateKeyword('') } handleKeyDown (e) { - // reset states - this.setState({ - isAlphabet: false, - isIME: false - }) + // Re-apply search field on ENTER key + if (e.keyCode === 13) { + this.debouncedUpdateKeyword(e.target.value) + } // Clear search on ESC if (e.keyCode === 27) { @@ -84,51 +96,11 @@ class TopBar extends React.Component { ee.emit('list:prior') e.preventDefault() } - - // When the key is an alphabet, del, enter or ctr - if (e.keyCode <= 90 || e.keyCode >= 186 && e.keyCode <= 222) { - this.setState({ - isAlphabet: true - }) - // When the key is an IME input (Japanese, Chinese) - } else if (e.keyCode === 229) { - this.setState({ - isIME: true - }) - } - } - - handleKeyUp (e) { - // reset states - this.setState({ - isConfirmTranslation: false - }) - - // When the key is translation confirmation (Enter, Space) - if (this.state.isIME && (e.keyCode === 32 || e.keyCode === 13)) { - this.setState({ - isConfirmTranslation: true - }) - const keyword = this.refs.searchInput.value - this.updateKeyword(keyword) - } } handleSearchChange (e) { - if (this.state.isAlphabet || this.state.isConfirmTranslation) { - const keyword = this.refs.searchInput.value - this.updateKeyword(keyword) - } else { - e.preventDefault() - } - } - - updateKeyword (keyword) { - this.context.router.push(`/searched/${encodeURIComponent(keyword)}`) - this.setState({ - search: keyword - }) - ee.emit('top:search', keyword) + const keyword = e.target.value + this.debouncedUpdateKeyword(keyword) } handleSearchFocus (e) { @@ -136,6 +108,7 @@ class TopBar extends React.Component { isSearching: true }) } + handleSearchBlur (e) { e.stopPropagation() @@ -165,7 +138,7 @@ class TopBar extends React.Component { } handleCodeInit () { - ee.emit('top:search', this.refs.searchInput.value) + ee.emit('top:search', this.refs.searchInput.value || '') } render () { @@ -178,24 +151,23 @@ class TopBar extends React.Component {
this.handleSearchFocus(e)} - onBlur={(e) => this.handleSearchBlur(e)} + onFocus={this.handleSearchFocus} + onBlur={this.handleSearchBlur} tabIndex='-1' ref='search' > - this.handleSearchChange(e)} - onKeyDown={(e) => this.handleKeyDown(e)} - onKeyUp={(e) => this.handleKeyUp(e)} + onInputChange={this.handleSearchChange} + onKeyDown={this.handleKeyDown} placeholder={i18n.__('Search')} type='text' className='searchInput' /> {this.state.search !== '' &&
diff --git a/browser/main/index.js b/browser/main/index.js index 6e8bdcc5..b3a909e5 100644 --- a/browser/main/index.js +++ b/browser/main/index.js @@ -1,11 +1,13 @@ import { Provider } from 'react-redux' import Main from './Main' -import store from './store' -import React from 'react' +import { store, history } from './store' +import React, { Fragment } from 'react' import ReactDOM from 'react-dom' require('!!style!css!stylus?sourceMap!./global.styl') -import { Router, Route, IndexRoute, IndexRedirect, hashHistory } from 'react-router' -import { syncHistoryWithStore } from 'react-router-redux' +import { Route, Switch, Redirect } from 'react-router-dom' +import { ConnectedRouter } from 'connected-react-router' +import DevTools from './DevTools' + require('./lib/ipcClient') require('../lib/customMeta') import i18n from 'browser/lib/i18n' @@ -77,7 +79,6 @@ document.addEventListener('click', function (e) { }) const el = document.getElementById('content') -const history = syncHistoryWithStore(hashHistory, store) function notify (...args) { return new window.Notification(...args) @@ -98,29 +99,24 @@ function updateApp () { ReactDOM.render(( - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + {/* storages */} + + + + + + + ), el, function () { const loadingCover = document.getElementById('loadingCover') diff --git a/browser/main/lib/ConfigManager.js b/browser/main/lib/ConfigManager.js index f20b3d88..ce641b9a 100644 --- a/browser/main/lib/ConfigManager.js +++ b/browser/main/lib/ConfigManager.js @@ -8,9 +8,14 @@ const win = global.process.platform === 'win32' const electron = require('electron') const { ipcRenderer } = electron const consts = require('browser/lib/consts') +const electronConfig = new (require('electron-config'))() let isInitialized = false +const DEFAULT_MARKDOWN_LINT_CONFIG = `{ + "default": true +}` + export const DEFAULT_CONFIG = { zoom: 1, isSideNavFolded: false, @@ -22,11 +27,16 @@ export const DEFAULT_CONFIG = { sortTagsBy: 'ALPHABETICAL', // 'ALPHABETICAL', 'COUNTER' listStyle: 'DEFAULT', // 'DEFAULT', 'SMALL' amaEnabled: true, + autoUpdateEnabled: true, hotkey: { toggleMain: OSX ? 'Command + Alt + L' : 'Super + Alt + E', toggleMode: OSX ? 'Command + Alt + M' : 'Ctrl + M', deleteNote: OSX ? 'Command + Shift + Backspace' : 'Ctrl + Shift + Backspace', pasteSmartly: OSX ? 'Command + Shift + V' : 'Ctrl + Shift + V', + prettifyMarkdown: OSX ? 'Command + Shift + F' : 'Ctrl + Shift + F', + sortLines: OSX ? 'Command + Shift + S' : 'Ctrl + Shift + S', + insertDate: OSX ? 'Command + /' : 'Ctrl + /', + insertDateTime: OSX ? 'Command + Alt + /' : 'Ctrl + Shift + /', toggleMenuBar: 'Alt' }, ui: { @@ -44,6 +54,7 @@ export const DEFAULT_CONFIG = { fontFamily: win ? 'Consolas' : 'Monaco', indentType: 'space', indentSize: '2', + lineWrapping: true, enableRulers: false, rulers: [80, 120], displayLineNumbers: true, @@ -59,7 +70,16 @@ export const DEFAULT_CONFIG = { enableFrontMatterTitle: true, frontMatterTitleField: 'title', spellcheck: false, - enableSmartPaste: false + enableSmartPaste: false, + enableMarkdownLint: false, + customMarkdownLintConfig: DEFAULT_MARKDOWN_LINT_CONFIG, + prettierConfig: ` { + "trailingComma": "es5", + "tabWidth": 4, + "semi": false, + "singleQuote": true + }`, + deleteUnusedAttachments: true }, preview: { fontSize: '14', @@ -77,8 +97,10 @@ export const DEFAULT_CONFIG = { breaks: true, smartArrows: false, allowCustomCSS: false, - customCSS: '', + + customCSS: '/* Drop Your Custom CSS Code Here */', sanitize: 'STRICT', // 'STRICT', 'ALLOW_STYLES', 'NONE' + mermaidHTMLLabel: false, lineThroughCheckbox: true }, blog: { @@ -102,7 +124,6 @@ function validate (config) { } function _save (config) { - console.log(config) window.localStorage.setItem('config', JSON.stringify(config)) } @@ -122,6 +143,8 @@ function get () { _save(config) } + config.autoUpdateEnabled = electronConfig.get('autoUpdateEnabled', config.autoUpdateEnabled) + if (!isInitialized) { isInitialized = true let editorTheme = document.getElementById('editorTheme') @@ -135,7 +158,7 @@ function get () { const theme = consts.THEMES.find(theme => theme.name === config.editor.theme) if (theme) { - editorTheme.setAttribute('href', `../${theme.path}`) + editorTheme.setAttribute('href', theme.path) } else { config.editor.theme = 'default' } @@ -146,7 +169,13 @@ function get () { function set (updates) { const currentConfig = get() - const newConfig = Object.assign({}, DEFAULT_CONFIG, currentConfig, updates) + + const arrangedUpdates = updates + if (updates.preview !== undefined && updates.preview.customCSS === '') { + arrangedUpdates.preview.customCSS = DEFAULT_CONFIG.preview.customCSS + } + + const newConfig = Object.assign({}, DEFAULT_CONFIG, currentConfig, arrangedUpdates) if (!validate(newConfig)) throw new Error('INVALID CONFIG') _save(newConfig) @@ -177,9 +206,11 @@ function set (updates) { const newTheme = consts.THEMES.find(theme => theme.name === newConfig.editor.theme) if (newTheme) { - editorTheme.setAttribute('href', `../${newTheme.path}`) + editorTheme.setAttribute('href', newTheme.path) } + electronConfig.set('autoUpdateEnabled', newConfig.autoUpdateEnabled) + ipcRenderer.send('config-renew', { config: get() }) diff --git a/browser/main/lib/dataApi/attachmentManagement.js b/browser/main/lib/dataApi/attachmentManagement.js index d92a1eb4..971ae812 100644 --- a/browser/main/lib/dataApi/attachmentManagement.js +++ b/browser/main/lib/dataApi/attachmentManagement.js @@ -8,6 +8,7 @@ const escapeStringRegexp = require('escape-string-regexp') const sander = require('sander') const url = require('url') import i18n from 'browser/lib/i18n' +import { isString } from 'lodash' const STORAGE_FOLDER_PLACEHOLDER = ':storage' const DESTINATION_FOLDER = 'attachments' @@ -19,7 +20,7 @@ const PATH_SEPARATORS = escapeStringRegexp(path.posix.sep) + escapeStringRegexp( * @returns {Promise} Image element created */ function getImage (file) { - if (_.isString(file)) { + if (isString(file)) { return new Promise(resolve => { const img = new Image() img.onload = () => resolve(img) @@ -241,6 +242,10 @@ function migrateAttachments (markdownContent, storagePath, noteKey) { * @returns {String} postprocessed HTML in which all :storage references are mapped to the actual paths. */ function fixLocalURLS (renderedHTML, storagePath) { + const encodedWin32SeparatorRegex = /%5C/g + const storageRegex = new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER, 'g') + const storageUrl = 'file:///' + path.join(storagePath, DESTINATION_FOLDER).replace(/\\/g, '/') + /* A :storage reference is like `:storage/3b6f8bd6-4edd-4b15-96e0-eadc4475b564/f939b2c3.jpg`. @@ -250,8 +255,7 @@ function fixLocalURLS (renderedHTML, storagePath) { - `(?:\\\/|%5C)` match the path seperator. `\\\/` for posix systems and `%5C` for windows. */ return renderedHTML.replace(new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER + '(?:(?:\\\/|%5C)[-.\\w]+)+', 'g'), function (match) { - var encodedPathSeparators = new RegExp(mdurl.encode(path.win32.sep) + '|' + mdurl.encode(path.posix.sep), 'g') - return match.replace(encodedPathSeparators, path.sep).replace(new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER, 'g'), 'file:///' + path.join(storagePath, DESTINATION_FOLDER)) + return match.replace(encodedWin32SeparatorRegex, '/').replace(storageRegex, storageUrl) }) } @@ -617,11 +621,79 @@ function deleteAttachmentsNotPresentInNote (markdownContent, storageKey, noteKey } }) }) - } else { - console.info('Attachment folder ("' + attachmentFolder + '") did not exist..') } } +/** + * @description Get all existing attachments related to a specific note + including their status (in use or not) and their path. Return null if there're no attachment related to note or specified parametters are invalid + * @param markdownContent markdownContent of the current note + * @param storageKey StorageKey of the current note + * @param noteKey NoteKey of the currentNote + * @return {Promise>} Promise returning the + list of attachments with their properties */ +function getAttachmentsPathAndStatus (markdownContent, storageKey, noteKey) { + if (storageKey == null || noteKey == null || markdownContent == null) { + return null + } + const targetStorage = findStorage.findStorage(storageKey) + const attachmentFolder = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey) + const attachmentsInNote = getAttachmentsInMarkdownContent(markdownContent) + const attachmentsInNoteOnlyFileNames = [] + if (attachmentsInNote) { + for (let i = 0; i < attachmentsInNote.length; i++) { + attachmentsInNoteOnlyFileNames.push(attachmentsInNote[i].replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER + escapeStringRegexp(path.sep) + noteKey + escapeStringRegexp(path.sep), 'g'), '')) + } + } + if (fs.existsSync(attachmentFolder)) { + return new Promise((resolve, reject) => { + fs.readdir(attachmentFolder, (err, files) => { + if (err) { + console.error('Error reading directory "' + attachmentFolder + '". Error:') + console.error(err) + reject(err) + return + } + const attachments = [] + for (const file of files) { + const absolutePathOfFile = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey, file) + if (!attachmentsInNoteOnlyFileNames.includes(file)) { + attachments.push({ path: absolutePathOfFile, isInUse: false }) + } else { + attachments.push({ path: absolutePathOfFile, isInUse: true }) + } + } + resolve(attachments) + }) + }) + } else { + return null + } +} + +/** + * @description Remove all specified attachment paths + * @param attachments attachment paths + * @return {Promise} Promise after all attachments are removed */ +function removeAttachmentsByPaths (attachments) { + const promises = [] + for (const attachment of attachments) { + const promise = new Promise((resolve, reject) => { + fs.unlink(attachment, (err) => { + if (err) { + console.error('Could not delete "%s"', attachment) + console.error(err) + reject(err) + return + } + resolve() + }) + }) + promises.push(promise) + } + return Promise.all(promises) +} + /** * Clones the attachments of a given note. * Copies the attachments to their new destination and updates the content of the new note so that the attachment-links again point to the correct destination. @@ -724,8 +796,10 @@ module.exports = { getAbsolutePathsOfAttachmentsInContent, importAttachments, removeStorageAndNoteReferences, + removeAttachmentsByPaths, deleteAttachmentFolder, deleteAttachmentsNotPresentInNote, + getAttachmentsPathAndStatus, moveAttachments, cloneAttachments, isAttachmentLink, diff --git a/browser/main/lib/dataApi/createNoteFromUrl.js b/browser/main/lib/dataApi/createNoteFromUrl.js new file mode 100644 index 00000000..ead93f9e --- /dev/null +++ b/browser/main/lib/dataApi/createNoteFromUrl.js @@ -0,0 +1,86 @@ +const http = require('http') +const https = require('https') +const { createTurndownService } = require('../../../lib/turndown') +const createNote = require('./createNote') + +import { push } from 'connected-react-router' +import ee from 'browser/main/lib/eventEmitter' + +function validateUrl (str) { + if (/^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})).?)(?::\d{2,5})?(?:[/?#]\S*)?$/i.test(str)) { + return true + } else { + return false + } +} + +const ERROR_MESSAGES = { + ENOTFOUND: 'URL not found. Please check the URL, or your internet connection and try again.', + VALIDATION_ERROR: 'Please check if the URL follows this format: https://www.google.com', + UNEXPECTED: 'Unexpected error! Please check console for details!' +} + +function createNoteFromUrl (url, storage, folder, dispatch = null, location = null) { + return new Promise((resolve, reject) => { + const td = createTurndownService() + + if (!validateUrl(url)) { + reject({result: false, error: ERROR_MESSAGES.VALIDATION_ERROR}) + } + + const request = url.startsWith('https') ? https : http + + const req = request.request(url, (res) => { + let data = '' + + res.on('data', (chunk) => { + data += chunk + }) + + res.on('end', () => { + const markdownHTML = td.turndown(data) + + if (dispatch !== null) { + createNote(storage, { + type: 'MARKDOWN_NOTE', + folder: folder, + title: '', + content: markdownHTML + }) + .then((note) => { + const noteHash = note.key + dispatch({ + type: 'UPDATE_NOTE', + note: note + }) + dispatch(push({ + pathname: location.pathname, + query: {key: noteHash} + })) + ee.emit('list:jump', noteHash) + ee.emit('detail:focus') + resolve({result: true, error: null}) + }) + } else { + createNote(storage, { + type: 'MARKDOWN_NOTE', + folder: folder, + title: '', + content: markdownHTML + }).then((note) => { + resolve({result: true, note, error: null}) + }) + } + }) + }) + + req.on('error', (e) => { + console.error('error in parsing URL', e) + reject({result: false, error: ERROR_MESSAGES[e.code] || ERROR_MESSAGES.UNEXPECTED}) + }) + + req.end() + }) +} + +module.exports = createNoteFromUrl diff --git a/browser/main/lib/dataApi/deleteFolder.js b/browser/main/lib/dataApi/deleteFolder.js index 0c7486f5..5ccc1414 100644 --- a/browser/main/lib/dataApi/deleteFolder.js +++ b/browser/main/lib/dataApi/deleteFolder.js @@ -3,7 +3,6 @@ const path = require('path') const resolveStorageData = require('./resolveStorageData') const resolveStorageNotes = require('./resolveStorageNotes') const CSON = require('@rokt33r/season') -const sander = require('sander') const { findStorage } = require('browser/lib/findStorage') const deleteSingleNote = require('./deleteNote') diff --git a/browser/main/lib/dataApi/exportNote.js b/browser/main/lib/dataApi/exportNote.js index 75c451c1..42e1fa56 100755 --- a/browser/main/lib/dataApi/exportNote.js +++ b/browser/main/lib/dataApi/exportNote.js @@ -43,7 +43,7 @@ function exportNote (nodeKey, storageKey, noteContent, targetPath, outputFormatt ) if (outputFormatter) { - exportedData = outputFormatter(exportedData, exportTasks, path.dirname(targetPath)) + exportedData = outputFormatter(exportedData, exportTasks, targetPath) } else { exportedData = Promise.resolve(exportedData) } diff --git a/browser/main/lib/dataApi/index.js b/browser/main/lib/dataApi/index.js index 92be6b93..6e88bbf9 100644 --- a/browser/main/lib/dataApi/index.js +++ b/browser/main/lib/dataApi/index.js @@ -11,6 +11,7 @@ const dataApi = { exportFolder: require('./exportFolder'), exportStorage: require('./exportStorage'), createNote: require('./createNote'), + createNoteFromUrl: require('./createNoteFromUrl'), updateNote: require('./updateNote'), deleteNote: require('./deleteNote'), moveNote: require('./moveNote'), diff --git a/browser/main/lib/dataApi/moveNote.js b/browser/main/lib/dataApi/moveNote.js index 2d306cdf..c38968cb 100644 --- a/browser/main/lib/dataApi/moveNote.js +++ b/browser/main/lib/dataApi/moveNote.js @@ -1,7 +1,6 @@ const resolveStorageData = require('./resolveStorageData') const _ = require('lodash') const path = require('path') -const fs = require('fs') const CSON = require('@rokt33r/season') const keygen = require('browser/lib/keygen') const sander = require('sander') diff --git a/browser/main/lib/modal.js b/browser/main/lib/modal.js index 7a7a9c8c..955cb5c8 100644 --- a/browser/main/lib/modal.js +++ b/browser/main/lib/modal.js @@ -1,7 +1,7 @@ import React from 'react' import { Provider } from 'react-redux' import ReactDOM from 'react-dom' -import store from '../store' +import { store } from '../store' class ModalBase extends React.Component { constructor (props) { diff --git a/browser/main/modals/CreateFolderModal.js b/browser/main/modals/CreateFolderModal.js index b061b0f3..b48d6e42 100644 --- a/browser/main/modals/CreateFolderModal.js +++ b/browser/main/modals/CreateFolderModal.js @@ -3,7 +3,7 @@ import React from 'react' import CSSModules from 'browser/lib/CSSModules' import styles from './CreateFolderModal.styl' import dataApi from 'browser/main/lib/dataApi' -import store from 'browser/main/store' +import { store } from 'browser/main/store' import consts from 'browser/lib/consts' import ModalEscButton from 'browser/components/ModalEscButton' import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig' diff --git a/browser/main/modals/CreateMarkdownFromURLModal.js b/browser/main/modals/CreateMarkdownFromURLModal.js new file mode 100644 index 00000000..31988059 --- /dev/null +++ b/browser/main/modals/CreateMarkdownFromURLModal.js @@ -0,0 +1,118 @@ +import PropTypes from 'prop-types' +import React from 'react' +import CSSModules from 'browser/lib/CSSModules' +import styles from './CreateMarkdownFromURLModal.styl' +import dataApi from 'browser/main/lib/dataApi' +import ModalEscButton from 'browser/components/ModalEscButton' +import i18n from 'browser/lib/i18n' + +class CreateMarkdownFromURLModal extends React.Component { + constructor (props) { + super(props) + + this.state = { + name: '', + showerror: false, + errormessage: '' + } + } + + componentDidMount () { + this.refs.name.focus() + this.refs.name.select() + } + + handleCloseButtonClick (e) { + this.props.close() + } + + handleChange (e) { + this.setState({ + name: this.refs.name.value + }) + } + + handleKeyDown (e) { + if (e.keyCode === 27) { + this.props.close() + } + } + + handleInputKeyDown (e) { + switch (e.keyCode) { + case 13: + this.confirm() + } + } + + handleConfirmButtonClick (e) { + this.confirm() + } + + showError (message) { + this.setState({ + showerror: true, + errormessage: message + }) + } + + hideError () { + this.setState({ + showerror: false, + errormessage: '' + }) + } + + confirm () { + this.hideError() + const { storage, folder, dispatch, location } = this.props + + dataApi.createNoteFromUrl(this.state.name, storage, folder, dispatch, location).then((result) => { + this.props.close() + }).catch((result) => { + this.showError(result.error) + }) + } + + render () { + return ( +
this.handleKeyDown(e)} + > +
+
{i18n.__('Import Markdown From URL')}
+
+ this.handleCloseButtonClick(e)} /> +
+
+
{i18n.__('Insert URL Here')}
+ this.handleChange(e)} + onKeyDown={(e) => this.handleInputKeyDown(e)} + /> +
+ +
{this.state.errormessage}
+
+
+ ) + } +} + +CreateMarkdownFromURLModal.propTypes = { + storage: PropTypes.string, + folder: PropTypes.string, + dispatch: PropTypes.func, + location: PropTypes.shape({ + pathname: PropTypes.string + }) +} + +export default CSSModules(CreateMarkdownFromURLModal, styles) diff --git a/browser/main/modals/CreateMarkdownFromURLModal.styl b/browser/main/modals/CreateMarkdownFromURLModal.styl new file mode 100644 index 00000000..5e59e465 --- /dev/null +++ b/browser/main/modals/CreateMarkdownFromURLModal.styl @@ -0,0 +1,160 @@ +.root + modal() + width 500px + height 270px + overflow hidden + position relative + +.header + height 80px + margin-bottom 10px + margin-top 20px + font-size 18px + line-height 50px + background-color $ui-backgroundColor + color $ui-text-color + +.title + font-size 36px + font-weight 600 + +.control-folder-label + text-align left + font-size 14px + color $ui-text-color + +.control-folder-input + display block + height 40px + width 490px + padding 0 5px + margin 10px 0 + border 1px solid $ui-input--create-folder-modal + border-radius 2px + background-color transparent + outline none + vertical-align middle + font-size 16px + &:disabled + background-color $ui-input--disabled-backgroundColor + &:focus, &:active + border-color $ui-active-color + +.control-confirmButton + display block + height 35px + width 140px + border none + border-radius 2px + padding 0 25px + margin 20px auto + font-size 14px + colorPrimaryButton() + +body[data-theme="dark"] + .root + modalDark() + width 500px + height 270px + overflow hidden + position relative + + .header + background-color transparent + border-color $ui-dark-borderColor + color $ui-dark-text-color + + .control-folder-label + color $ui-dark-text-color + + .control-folder-input + border 1px solid $ui-input--create-folder-modal + color white + + .description + color $ui-inactive-text-color + + .control-confirmButton + colorDarkPrimaryButton() + +body[data-theme="solarized-dark"] + .root + modalSolarizedDark() + width 500px + height 270px + overflow hidden + position relative + + .header + background-color transparent + border-color $ui-dark-borderColor + color $ui-solarized-dark-text-color + + .control-folder-label + color $ui-solarized-dark-text-color + + .control-folder-input + border 1px solid $ui-input--create-folder-modal + color white + + .description + color $ui-inactive-text-color + + .control-confirmButton + colorSolarizedDarkPrimaryButton() + +.error + text-align center + color #F44336 + +body[data-theme="monokai"] + .root + modalMonokai() + width 500px + height 270px + overflow hidden + position relative + + .header + background-color transparent + border-color $ui-dark-borderColor + color $ui-monokai-text-color + + .control-folder-label + color $ui-monokai-text-color + + .control-folder-input + border 1px solid $ui-input--create-folder-modal + color white + + .description + color $ui-inactive-text-color + + .control-confirmButton + colorMonokaiPrimaryButton() + +body[data-theme="dracula"] + .root + modalDracula() + width 500px + height 270px + overflow hidden + position relative + + .header + background-color transparent + border-color $ui-dracula-borderColor + color $ui-dracula-text-color + + .control-folder-label + color $ui-dracula-text-color + + .control-folder-input + border 1px solid $ui-input--create-folder-modal + color white + + .description + color $ui-inactive-text-color + + .control-confirmButton + colorDraculaPrimaryButton() diff --git a/browser/main/modals/NewNoteModal.js b/browser/main/modals/NewNoteModal.js index 41c174cb..476fa252 100644 --- a/browser/main/modals/NewNoteModal.js +++ b/browser/main/modals/NewNoteModal.js @@ -3,7 +3,10 @@ import CSSModules from 'browser/lib/CSSModules' import styles from './NewNoteModal.styl' import ModalEscButton from 'browser/components/ModalEscButton' import i18n from 'browser/lib/i18n' +import { openModal } from 'browser/main/lib/modal' +import CreateMarkdownFromURLModal from '../modals/CreateMarkdownFromURLModal' import { createMarkdownNote, createSnippetNote } from 'browser/lib/newNote' +import queryString from 'query-string' class NewNoteModal extends React.Component { constructor (props) { @@ -20,8 +23,21 @@ class NewNoteModal extends React.Component { this.props.close() } + handleCreateMarkdownFromUrlClick (e) { + this.props.close() + + const { storage, folder, dispatch, location } = this.props + openModal(CreateMarkdownFromURLModal, { + storage: storage, + folder: folder, + dispatch, + location + }) + } + handleMarkdownNoteButtonClick (e) { - const { storage, folder, dispatch, location, params, config } = this.props + const { storage, folder, dispatch, location, config } = this.props + const params = location.search !== '' && queryString.parse(location.search) if (!this.lock) { this.lock = true createMarkdownNote(storage, folder, dispatch, location, params, config).then(() => { @@ -38,7 +54,8 @@ class NewNoteModal extends React.Component { } handleSnippetNoteButtonClick (e) { - const { storage, folder, dispatch, location, params, config } = this.props + const { storage, folder, dispatch, location, config } = this.props + const params = location.search !== '' && queryString.parse(location.search) if (!this.lock) { this.lock = true createSnippetNote(storage, folder, dispatch, location, params, config).then(() => { @@ -112,10 +129,8 @@ class NewNoteModal extends React.Component {
-
- {i18n.__('Tab to switch format')} -
- +
{i18n.__('Tab to switch format')}
+
this.handleCreateMarkdownFromUrlClick(e)}>Or, create a new markdown note from a URL
) } diff --git a/browser/main/modals/NewNoteModal.styl b/browser/main/modals/NewNoteModal.styl index c82b9376..ff0052bd 100644 --- a/browser/main/modals/NewNoteModal.styl +++ b/browser/main/modals/NewNoteModal.styl @@ -19,6 +19,7 @@ .control padding 25px 0px text-align center + display: flex .control-button width 240px @@ -47,6 +48,12 @@ text-align center margin-bottom 25px +.from-url + color $ui-inactive-text-color + text-align center + margin-bottom 25px + cursor pointer + body[data-theme="dark"] .root modalDark() @@ -61,7 +68,7 @@ body[data-theme="dark"] &:focus colorDarkPrimaryButton() - .description + .description, .from-url color $ui-inactive-text-color body[data-theme="solarized-dark"] @@ -78,7 +85,7 @@ body[data-theme="solarized-dark"] &:focus colorDarkPrimaryButton() - .description + .description, .from-url color $ui-solarized-dark-text-color body[data-theme="monokai"] @@ -95,7 +102,7 @@ body[data-theme="monokai"] &:focus colorDarkPrimaryButton() - .description + .description, .from-url color $ui-monokai-text-color body[data-theme="dracula"] diff --git a/browser/main/modals/PreferencesModal/Blog.js b/browser/main/modals/PreferencesModal/Blog.js index 2c93fb29..4d59bea1 100644 --- a/browser/main/modals/PreferencesModal/Blog.js +++ b/browser/main/modals/PreferencesModal/Blog.js @@ -2,7 +2,7 @@ import React from 'react' import CSSModules from 'browser/lib/CSSModules' import styles from './ConfigTab.styl' import ConfigManager from 'browser/main/lib/ConfigManager' -import store from 'browser/main/store' +import { store } from 'browser/main/store' import PropTypes from 'prop-types' import _ from 'lodash' import i18n from 'browser/lib/i18n' diff --git a/browser/main/modals/PreferencesModal/FolderItem.js b/browser/main/modals/PreferencesModal/FolderItem.js index dc9082b9..648db4e6 100644 --- a/browser/main/modals/PreferencesModal/FolderItem.js +++ b/browser/main/modals/PreferencesModal/FolderItem.js @@ -4,7 +4,7 @@ import CSSModules from 'browser/lib/CSSModules' import ReactDOM from 'react-dom' import styles from './FolderItem.styl' import dataApi from 'browser/main/lib/dataApi' -import store from 'browser/main/store' +import { store } from 'browser/main/store' import { SketchPicker } from 'react-color' import { SortableElement, SortableHandle } from 'react-sortable-hoc' import i18n from 'browser/lib/i18n' @@ -225,7 +225,7 @@ class FolderItem extends React.Component {
- {folder.name} + {folder.name} ({folder.key})
@@ -288,10 +288,10 @@ class Handle extends React.Component { class SortableFolderItemComponent extends React.Component { render () { - const StyledHandle = CSSModules(Handle, this.props.styles) + const StyledHandle = CSSModules(Handle, styles) const DragHandle = SortableHandle(StyledHandle) - const StyledFolderItem = CSSModules(FolderItem, this.props.styles) + const StyledFolderItem = CSSModules(FolderItem, styles) return (
diff --git a/browser/main/modals/PreferencesModal/FolderList.js b/browser/main/modals/PreferencesModal/FolderList.js index e7cc6f94..674026c5 100644 --- a/browser/main/modals/PreferencesModal/FolderList.js +++ b/browser/main/modals/PreferencesModal/FolderList.js @@ -3,7 +3,7 @@ import React from 'react' import CSSModules from 'browser/lib/CSSModules' import dataApi from 'browser/main/lib/dataApi' import styles from './FolderList.styl' -import store from 'browser/main/store' +import { store } from 'browser/main/store' import FolderItem from './FolderItem' import { SortableContainer } from 'react-sortable-hoc' import i18n from 'browser/lib/i18n' @@ -22,7 +22,7 @@ class FolderList extends React.Component { }) return ( -
+
{folderList.length > 0 ? folderList :
{i18n.__('No Folders')}
diff --git a/browser/main/modals/PreferencesModal/HotkeyTab.js b/browser/main/modals/PreferencesModal/HotkeyTab.js index 218a68f6..9c4f5655 100644 --- a/browser/main/modals/PreferencesModal/HotkeyTab.js +++ b/browser/main/modals/PreferencesModal/HotkeyTab.js @@ -3,7 +3,7 @@ import React from 'react' import CSSModules from 'browser/lib/CSSModules' import styles from './ConfigTab.styl' import ConfigManager from 'browser/main/lib/ConfigManager' -import store from 'browser/main/store' +import { store } from 'browser/main/store' import _ from 'lodash' import i18n from 'browser/lib/i18n' @@ -76,13 +76,16 @@ class HotkeyTab extends React.Component { handleHotkeyChange (e) { const { config } = this.state - config.hotkey = { + config.hotkey = Object.assign({}, config.hotkey, { toggleMain: this.refs.toggleMain.value, toggleMode: this.refs.toggleMode.value, deleteNote: this.refs.deleteNote.value, pasteSmartly: this.refs.pasteSmartly.value, - toggleMenuBar: this.refs.toggleMenuBar.value - } + prettifyMarkdown: this.refs.prettifyMarkdown.value, + toggleMenuBar: this.refs.toggleMenuBar.value, + insertDate: this.refs.insertDate.value, + insertDateTime: this.refs.insertDateTime.value + }) this.setState({ config }) @@ -173,6 +176,38 @@ class HotkeyTab extends React.Component { />
+
+
{i18n.__('Prettify Markdown')}
+
+ this.handleHotkeyChange(e)} + ref='prettifyMarkdown' + value={config.hotkey.prettifyMarkdown} + type='text' /> +
+
+
+
{i18n.__('Insert Current Date')}
+
+ +
+
+
+
{i18n.__('Insert Current Date and Time')}
+
+ +
+
+
{i18n.__('Attachment storage')}
+

+ Unused attachments size: {humanFileSize(totalUnusedAttachmentsSize)} ({totalUnusedAttachments} items) +

+

+ In use attachments size: {humanFileSize(totalInuseAttachmentsSize)} ({totalInuseAttachments} items) +

+

+ Total attachments size: {humanFileSize(totalAttachmentsSize)} ({totalAttachments} items) +

+
) } diff --git a/browser/main/modals/PreferencesModal/StoragesTab.styl b/browser/main/modals/PreferencesModal/StoragesTab.styl index b63cc85e..fbfa89e6 100644 --- a/browser/main/modals/PreferencesModal/StoragesTab.styl +++ b/browser/main/modals/PreferencesModal/StoragesTab.styl @@ -33,6 +33,17 @@ colorDefaultButton() font-size $tab--button-font-size border-radius 2px +.list-attachment-label + margin-bottom 10px + color $ui-text-color +.list-attachement-clear-button + height 30px + border none + border-top-right-radius 2px + border-bottom-right-radius 2px + colorPrimaryButton() + vertical-align middle + padding 0 20px .addStorage margin-bottom 15px @@ -154,8 +165,8 @@ body[data-theme="dark"] .addStorage-body-control-cancelButton colorDarkDefaultButton() border-color $ui-dark-borderColor - - + .list-attachement-clear-button + colorDarkPrimaryButton() body[data-theme="solarized-dark"] .root @@ -194,6 +205,8 @@ body[data-theme="solarized-dark"] .addStorage-body-control-cancelButton colorDarkDefaultButton() border-color $ui-solarized-dark-borderColor + .list-attachement-clear-button + colorSolarizedDarkPrimaryButton() body[data-theme="monokai"] .root @@ -232,6 +245,8 @@ body[data-theme="monokai"] .addStorage-body-control-cancelButton colorDarkDefaultButton() border-color $ui-monokai-borderColor + .list-attachement-clear-button + colorMonokaiPrimaryButton() body[data-theme="dracula"] .root @@ -269,4 +284,6 @@ body[data-theme="dracula"] colorDarkPrimaryButton() .addStorage-body-control-cancelButton colorDarkDefaultButton() - border-color $ui-dracula-borderColor \ No newline at end of file + border-color $ui-dracula-borderColor + .list-attachement-clear-button + colorDraculaPrimaryButton() \ No newline at end of file diff --git a/browser/main/modals/PreferencesModal/UiTab.js b/browser/main/modals/PreferencesModal/UiTab.js index 994ca3d3..329dbfa4 100644 --- a/browser/main/modals/PreferencesModal/UiTab.js +++ b/browser/main/modals/PreferencesModal/UiTab.js @@ -3,7 +3,7 @@ import React from 'react' import CSSModules from 'browser/lib/CSSModules' import styles from './ConfigTab.styl' import ConfigManager from 'browser/main/lib/ConfigManager' -import store from 'browser/main/store' +import { store } from 'browser/main/store' import consts from 'browser/lib/consts' import ReactCodeMirror from 'react-codemirror' import CodeMirror from 'codemirror' @@ -30,7 +30,13 @@ class UiTab extends React.Component { componentDidMount () { CodeMirror.autoLoadMode(this.codeMirrorInstance.getCodeMirror(), 'javascript') CodeMirror.autoLoadMode(this.customCSSCM.getCodeMirror(), 'css') + CodeMirror.autoLoadMode(this.customMarkdownLintConfigCM.getCodeMirror(), 'javascript') + CodeMirror.autoLoadMode(this.prettierConfigCM.getCodeMirror(), 'javascript') + // Set CM editor Sizes this.customCSSCM.getCodeMirror().setSize('400px', '400px') + this.prettierConfigCM.getCodeMirror().setSize('400px', '400px') + this.customMarkdownLintConfigCM.getCodeMirror().setSize('400px', '200px') + this.handleSettingDone = () => { this.setState({UiAlert: { type: 'success', @@ -89,6 +95,7 @@ class UiTab extends React.Component { enableRulers: this.refs.enableEditorRulers.value === 'true', rulers: this.refs.editorRulers.value.replace(/[^0-9,]/g, '').split(','), displayLineNumbers: this.refs.editorDisplayLineNumbers.checked, + lineWrapping: this.refs.editorLineWrapping.checked, switchPreview: this.refs.editorSwitchPreview.value, keyMap: this.refs.editorKeyMap.value, snippetDefaultLanguage: this.refs.editorSnippetDefaultLanguage.value, @@ -101,7 +108,11 @@ class UiTab extends React.Component { matchingTriples: this.refs.matchingTriples.value, explodingPairs: this.refs.explodingPairs.value, spellcheck: this.refs.spellcheck.checked, - enableSmartPaste: this.refs.enableSmartPaste.checked + enableSmartPaste: this.refs.enableSmartPaste.checked, + enableMarkdownLint: this.refs.enableMarkdownLint.checked, + customMarkdownLintConfig: this.customMarkdownLintConfigCM.getCodeMirror().getValue(), + prettierConfig: this.prettierConfigCM.getCodeMirror().getValue(), + deleteUnusedAttachments: this.refs.deleteUnusedAttachments.checked }, preview: { fontSize: this.refs.previewFontSize.value, @@ -119,6 +130,7 @@ class UiTab extends React.Component { breaks: this.refs.previewBreaks.checked, smartArrows: this.refs.previewSmartArrows.checked, sanitize: this.refs.previewSanitize.value, + mermaidHTMLLabel: this.refs.previewMermaidHTMLLabel.checked, allowCustomCSS: this.refs.previewAllowCustomCSS.checked, lineThroughCheckbox: this.refs.lineThroughCheckbox.checked, customCSS: this.customCSSCM.getCodeMirror().getValue() @@ -131,7 +143,7 @@ class UiTab extends React.Component { const theme = consts.THEMES.find(theme => theme.name === newCodemirrorTheme) if (theme) { - checkHighLight.setAttribute('href', `../${theme.path}`) + checkHighLight.setAttribute('href', theme.path) } } @@ -541,6 +553,17 @@ class UiTab extends React.Component {
+
+ +
+
+
+ +
@@ -637,6 +670,34 @@ class UiTab extends React.Component { />
+
+
+ {i18n.__('Custom MarkdownLint Rules')} +
+
+ this.handleUIChange(e)} + checked={this.state.config.editor.enableMarkdownLint} + ref='enableMarkdownLint' + type='checkbox' + />  + {i18n.__('Enable MarkdownLint')} +
+ this.handleUIChange(e)} + ref={e => (this.customMarkdownLintConfigCM = e)} + value={config.editor.customMarkdownLintConfig} + options={{ + lineNumbers: true, + mode: 'application/json', + theme: codemirrorTheme, + lint: true, + gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter', 'CodeMirror-lint-markers'] + }} /> +
+
+
{i18n.__('Preview')}
@@ -768,6 +829,16 @@ class UiTab extends React.Component {
+
+ +
{i18n.__('LaTeX Inline Open Delimiter')} @@ -851,7 +922,6 @@ class UiTab extends React.Component { onChange={e => this.handleUIChange(e)} ref={e => (this.customCSSCM = e)} value={config.preview.customCSS} - defaultValue={'/* Drop Your Custom CSS Code Here */\n'} options={{ lineNumbers: true, mode: 'css', @@ -860,7 +930,27 @@ class UiTab extends React.Component {
- +
+
+ {i18n.__('Prettier Config')} +
+
+
+ this.handleUIChange(e)} + ref={e => (this.prettierConfigCM = e)} + value={config.editor.prettierConfig} + options={{ + lineNumbers: true, + mode: 'application/json', + lint: true, + theme: codemirrorTheme + }} /> +
+
+