diff --git a/.eslintrc b/.eslintrc index a460b507..1709c9d8 100644 --- a/.eslintrc +++ b/.eslintrc @@ -19,5 +19,8 @@ "FileReader": true, "localStorage": true, "fetch": true + }, + "env": { + "jest": true } } diff --git a/.travis.yml b/.travis.yml index c68d1063..d9267f77 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,9 @@ language: node_js node_js: - - 6 + - 7 script: - npm run lint && npm run test + - yarn jest - 'if [[ ${TRAVIS_PULL_REQUEST_BRANCH:-$TRAVIS_BRANCH} = "master" ]]; then npm install -g grunt npm@5.2 && 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 492ba7b3..98e25f1a 100644 --- a/browser/components/CodeEditor.js +++ b/browser/components/CodeEditor.js @@ -4,6 +4,7 @@ import _ from 'lodash' import CodeMirror from 'codemirror' import 'codemirror-mode-elixir' import attachmentManagement from 'browser/main/lib/dataApi/attachmentManagement' +import convertModeName from 'browser/lib/convertModeName' import eventEmitter from 'browser/main/lib/eventEmitter' import iconv from 'iconv-lite' import crypto from 'crypto' @@ -17,21 +18,6 @@ const defaultEditorFontFamily = ['Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', ' const buildCMRulers = (rulers, enableRulers) => enableRulers ? rulers.map(ruler => ({ column: ruler })) : [] -function pass (name) { - switch (name) { - case 'ejs': - return 'Embedded Javascript' - case 'html_ruby': - return 'Embedded Ruby' - case 'objectivec': - return 'Objective C' - case 'text': - return 'Plain Text' - default: - return name - } -} - export default class CodeEditor extends React.Component { constructor (props) { super(props) @@ -52,6 +38,9 @@ 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) } this.pasteHandler = (editor, e) => this.handlePaste(editor, e) this.loadStyleHandler = (e) => { @@ -323,7 +312,7 @@ export default class CodeEditor extends React.Component { } setMode (mode) { - let syntax = CodeMirror.findModeByName(pass(mode)) + let syntax = CodeMirror.findModeByName(convertModeName(mode)) if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text') this.editor.setOption('mode', syntax.mime) diff --git a/browser/components/MarkdownEditor.js b/browser/components/MarkdownEditor.js index 313c6f90..2bd5d951 100644 --- a/browser/components/MarkdownEditor.js +++ b/browser/components/MarkdownEditor.js @@ -283,6 +283,7 @@ class MarkdownEditor extends React.Component { indentSize={editorIndentSize} scrollPastEnd={config.preview.scrollPastEnd} smartQuotes={config.preview.smartQuotes} + breaks={config.preview.breaks} sanitize={config.preview.sanitize} ref='preview' onContextMenu={(e) => this.handleContextMenu(e)} @@ -294,6 +295,7 @@ class MarkdownEditor extends React.Component { onCheckboxClick={(e) => this.handleCheckboxClick(e)} showCopyNotification={config.ui.showCopyNotification} storagePath={storage.path} + noteKey={noteKey} /> ) diff --git a/browser/components/MarkdownPreview.js b/browser/components/MarkdownPreview.js index 55d85bea..991cd9d0 100755 --- a/browser/components/MarkdownPreview.js +++ b/browser/components/MarkdownPreview.js @@ -10,6 +10,7 @@ import flowchart from 'flowchart' import SequenceDiagram from 'js-sequence-diagrams' import eventEmitter from 'browser/main/lib/eventEmitter' import htmlTextHelper from 'browser/lib/htmlTextHelper' +import convertModeName from 'browser/lib/convertModeName' import copy from 'copy-to-clipboard' import mdurl from 'mdurl' import exportNote from 'browser/main/lib/dataApi/exportNote' @@ -31,7 +32,7 @@ const CSS_FILES = [ `${appPath}/node_modules/codemirror/lib/codemirror.css` ] -function buildStyle (fontFamily, fontSize, codeBlockFontFamily, lineNumber, scrollPastEnd) { +function buildStyle (fontFamily, fontSize, codeBlockFontFamily, lineNumber, scrollPastEnd, theme) { return ` @font-face { font-family: 'Lato'; @@ -103,6 +104,13 @@ h2 { body p { white-space: normal; } + +@media print { + body[data-theme="${theme}"] { + color: #000; + background-color: #fff; + } +} ` } @@ -115,7 +123,6 @@ if (!OSX) { defaultFontFamily.unshift('meiryo') } const defaultCodeBlockFontFamily = ['Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', 'monospace'] - export default class MarkdownPreview extends React.Component { constructor (props) { super(props) @@ -138,10 +145,11 @@ export default class MarkdownPreview extends React.Component { } initMarkdown () { - const { smartQuotes, sanitize } = this.props + const { smartQuotes, sanitize, breaks } = this.props this.markdown = new Markdown({ typographer: smartQuotes, - sanitize + sanitize, + breaks }) } @@ -208,11 +216,13 @@ export default class MarkdownPreview extends React.Component { handleSaveAsHtml () { this.exportAsDocument('html', (noteContent, exportTasks) => { - const {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme} = this.getStyleParams() + const {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, scrollPastEnd, theme} = this.getStyleParams() + + const inlineStyles = buildStyle(fontFamily, fontSize, codeBlockFontFamily, lineNumber, scrollPastEnd, theme) + let body = this.markdown.render(escapeHtmlCharacters(noteContent)) - const inlineStyles = buildStyle(fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, lineNumber) - const body = this.markdown.render(escapeHtmlCharacters(noteContent)) const files = [this.GetCodeThemeLink(codeBlockTheme), ...CSS_FILES] + const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent(noteContent, this.props.storagePath) files.forEach((file) => { file = file.replace('file://', '') @@ -221,6 +231,13 @@ export default class MarkdownPreview extends React.Component { dst: 'css' }) }) + attachmentsAbsolutePaths.forEach((attachment) => { + exportTasks.push({ + src: attachment, + dst: attachmentManagement.DESTINATION_FOLDER + }) + }) + body = attachmentManagement.removeStorageAndNoteReferences(body, this.props.noteKey) let styles = '' files.forEach((file) => { @@ -324,7 +341,9 @@ export default class MarkdownPreview extends React.Component { componentDidUpdate (prevProps) { if (prevProps.value !== this.props.value) this.rewriteIframe() - if (prevProps.smartQuotes !== this.props.smartQuotes || prevProps.sanitize !== this.props.sanitize) { + if (prevProps.smartQuotes !== this.props.smartQuotes || + prevProps.sanitize !== this.props.sanitize || + prevProps.breaks !== this.props.breaks) { this.initMarkdown() this.rewriteIframe() } @@ -342,7 +361,7 @@ export default class MarkdownPreview extends React.Component { } getStyleParams () { - const { fontSize, lineNumber, codeBlockTheme, scrollPastEnd } = this.props + const { fontSize, lineNumber, codeBlockTheme, scrollPastEnd, theme } = this.props let { fontFamily, codeBlockFontFamily } = this.props fontFamily = _.isString(fontFamily) && fontFamily.trim().length > 0 ? fontFamily.split(',').map(fontName => fontName.trim()).concat(defaultFontFamily) @@ -351,14 +370,14 @@ export default class MarkdownPreview extends React.Component { ? codeBlockFontFamily.split(',').map(fontName => fontName.trim()).concat(defaultCodeBlockFontFamily) : defaultCodeBlockFontFamily - return {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, scrollPastEnd} + return {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, scrollPastEnd, theme} } applyStyle () { - const {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, scrollPastEnd} = this.getStyleParams() + const {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, scrollPastEnd, theme} = this.getStyleParams() this.getWindow().document.getElementById('codeTheme').href = this.GetCodeThemeLink(codeBlockTheme) - this.getWindow().document.getElementById('style').innerHTML = buildStyle(fontFamily, fontSize, codeBlockFontFamily, lineNumber, scrollPastEnd) + this.getWindow().document.getElementById('style').innerHTML = buildStyle(fontFamily, fontSize, codeBlockFontFamily, lineNumber, scrollPastEnd, theme) } GetCodeThemeLink (theme) { @@ -414,7 +433,7 @@ export default class MarkdownPreview extends React.Component { : 'default' _.forEach(this.refs.root.contentWindow.document.querySelectorAll('.code code'), (el) => { - let syntax = CodeMirror.findModeByName(el.className) + let syntax = CodeMirror.findModeByName(convertModeName(el.className)) if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text') CodeMirror.requireMode(syntax.mode, () => { const content = htmlTextHelper.decodeEntities(el.innerHTML) @@ -526,21 +545,36 @@ export default class MarkdownPreview extends React.Component { return } - const noteHash = e.target.href.split('/').pop() + const linkHash = href.split('/').pop() + + 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) + + if (targetElement != null) { + this.getWindow().scrollTo(0, targetElement.offsetTop) + } + return + } + // this will match the new uuid v4 hash and the old hash // e.g. // :note:1c211eb7dcb463de6490 and // :note:7dd23275-f2b4-49cb-9e93-3454daf1af9c const regexIsNoteLink = /^:note:([a-zA-Z0-9-]{20,36})$/ - if (regexIsNoteLink.test(noteHash)) { - eventEmitter.emit('list:jump', noteHash.replace(':note:', '')) + if (regexIsNoteLink.test(linkHash)) { + eventEmitter.emit('list:jump', linkHash.replace(':note:', '')) + return } + // this will match the old link format storage.key-note.key // e.g. // 877f99c3268608328037-1c211eb7dcb463de6490 const regexIsLegacyNoteLink = /^(.{20})-(.{20})$/ - if (regexIsLegacyNoteLink.test(noteHash)) { - eventEmitter.emit('list:jump', noteHash.split('-')[1]) + if (regexIsLegacyNoteLink.test(linkHash)) { + eventEmitter.emit('list:jump', linkHash.split('-')[1]) + return } } @@ -568,5 +602,6 @@ MarkdownPreview.propTypes = { value: PropTypes.string, showCopyNotification: PropTypes.bool, storagePath: PropTypes.string, - smartQuotes: PropTypes.bool + smartQuotes: PropTypes.bool, + breaks: PropTypes.bool } diff --git a/browser/components/MarkdownSplitEditor.js b/browser/components/MarkdownSplitEditor.js index d82d4da3..2bee5c24 100644 --- a/browser/components/MarkdownSplitEditor.js +++ b/browser/components/MarkdownSplitEditor.js @@ -131,6 +131,7 @@ class MarkdownSplitEditor extends React.Component { lineNumber={config.preview.lineNumber} scrollPastEnd={config.preview.scrollPastEnd} smartQuotes={config.preview.smartQuotes} + breaks={config.preview.breaks} sanitize={config.preview.sanitize} ref='preview' tabInde='0' @@ -139,6 +140,7 @@ class MarkdownSplitEditor extends React.Component { onScroll={this.handleScroll.bind(this)} showCopyNotification={config.ui.showCopyNotification} storagePath={storage.path} + noteKey={noteKey} /> ) diff --git a/browser/components/NoteItem.styl b/browser/components/NoteItem.styl index 4067a6cd..017ef6d0 100644 --- a/browser/components/NoteItem.styl +++ b/browser/components/NoteItem.styl @@ -321,3 +321,76 @@ body[data-theme="solarized-dark"] .item-bottom-tagList-empty color $ui-inactive-text-color vertical-align middle + +body[data-theme="monokai"] + .root + border-color $ui-monokai-borderColor + background-color $ui-monokai-noteList-backgroundColor + + .item + border-color $ui-monokai-borderColor + background-color $ui-monokai-noteList-backgroundColor + &:hover + transition 0.15s + // background-color alpha($ui-monokai-noteList-backgroundColor, 20%) + color $ui-monokai-text-color + .item-title + .item-title-icon + .item-bottom-time + transition 0.15s + color $ui-monokai-text-color + .item-bottom-tagList-item + transition 0.15s + background-color alpha($ui-monokai-noteList-backgroundColor, 20%) + color $ui-monokai-text-color + &:active + transition 0.15s + background-color $ui-monokai-noteList-backgroundColor + color $ui-monokai-text-color + .item-title + .item-title-icon + .item-bottom-time + transition 0.15s + color $ui-monokai-text-color + .item-bottom-tagList-item + transition 0.15s + background-color alpha($ui-monokai-noteList-backgroundColor, 10%) + color $ui-monokai-text-color + + .item-wrapper + border-color alpha($ui-monokai-button-backgroundColor, 60%) + + .item--active + border-color $ui-monokai-borderColor + background-color $ui-monokai-button-backgroundColor + .item-wrapper + border-color transparent + .item-title + .item-title-icon + .item-bottom-time + color $ui-monokai-text-color + .item-bottom-tagList-item + background-color alpha(white, 10%) + color $ui-monokai-text-color + &:hover + // background-color alpha($ui-monokai-button--active-backgroundColor, 60%) + color #c0392b + .item-bottom-tagList-item + background-color alpha(#fff, 20%) + + .item-title + color $ui-inactive-text-color + + .item-title-icon + color $ui-inactive-text-color + + .item-title-empty + color $ui-inactive-text-color + + .item-bottom-tagList-item + background-color alpha($ui-dark-button--active-backgroundColor, 40%) + color $ui-inactive-text-color + + .item-bottom-tagList-empty + color $ui-inactive-text-color + vertical-align middle diff --git a/browser/components/NoteItemSimple.styl b/browser/components/NoteItemSimple.styl index 3097b82c..661751bc 100644 --- a/browser/components/NoteItemSimple.styl +++ b/browser/components/NoteItemSimple.styl @@ -104,6 +104,7 @@ body[data-theme="dark"] background-color alpha($ui-dark-button--active-backgroundColor, 20%) color $ui-dark-text-color .item-simple-title + .item-simple-title-empty .item-simple-title-icon .item-simple-bottom-time transition 0.15s @@ -117,6 +118,7 @@ body[data-theme="dark"] background-color $ui-dark-button--active-backgroundColor color $ui-dark-text-color .item-simple-title + .item-simple-title-empty .item-simple-title-icon .item-simple-bottom-time transition 0.15s @@ -165,9 +167,10 @@ body[data-theme="solarized-dark"] background-color $ui-solarized-dark-noteList-backgroundColor &:hover transition 0.15s - // background-color alpha($ui-dark-button--active-backgroundColor, 20%) + background-color alpha($ui-dark-button--active-backgroundColor, 60%) color $ui-solarized-dark-text-color .item-simple-title + .item-simple-title-empty .item-simple-title-icon .item-simple-bottom-time transition 0.15s @@ -178,9 +181,10 @@ body[data-theme="solarized-dark"] color $ui-solarized-dark-text-color &:active transition 0.15s - background-color $ui-solarized-dark-button--active-backgroundColor - color $ui-solarized-dark-text-color + // background-color $ui-solarized-dark-button--active-backgroundColor + color $ui-dark-text-color .item-simple-title + .item-simple-title-empty .item-simple-title-icon .item-simple-bottom-time transition 0.15s @@ -192,11 +196,13 @@ body[data-theme="solarized-dark"] .item-simple--active border-color $ui-solarized-dark-borderColor - background-color $ui-solarized-dark-button--active-backgroundColor + background-color $ui-solarized-dark-tag-backgroundColor .item-simple-wrapper border-color transparent .item-simple-title + .item-simple-title-empty .item-simple-title-icon + color $ui-dark-text-color .item-simple-bottom-time color $ui-solarized-dark-text-color .item-simple-bottom-tagList-item @@ -207,8 +213,75 @@ body[data-theme="solarized-dark"] color #c0392b .item-simple-bottom-tagList-item background-color alpha(#fff, 20%) -.item-simple-right - float right - .item-simple-right-storageName - padding-left 4px - opacity 0.4 + .item-simple-title + color $ui-dark-text-color + border-bottom $ui-dark-borderColor + .item-simple-right + float right + .item-simple-right-storageName + padding-left 4px + opacity 0.4 + +body[data-theme="monokai"] + .root + border-color $ui-monokai-borderColor + background-color $ui-monokai-noteList-backgroundColor + + .item-simple + border-color $ui-monokai-borderColor + background-color $ui-monokai-noteList-backgroundColor + &:hover + transition 0.15s + background-color alpha($ui-monokai-button-backgroundColor, 60%) + color $ui-monokai-text-color + .item-simple-title + .item-simple-title-empty + .item-simple-title-icon + .item-simple-bottom-time + transition 0.15s + color $ui-solarized-dark-text-color + .item-simple-bottom-tagList-item + transition 0.15s + background-color alpha(#fff, 20%) + color $ui-monokai-text-color + &:active + transition 0.15s + background-color $ui-monokai-button--active-backgroundColor + color $ui-monokai-text-color + .item-simple-title + .item-simple-title-empty + .item-simple-title-icon + .item-simple-bottom-time + transition 0.15s + color $ui-monokai-text-color + .item-simple-bottom-tagList-item + transition 0.15s + background-color alpha(white, 10%) + color $ui-monokai-text-color + + .item-simple--active + border-color $ui-monokai-borderColor + background-color $ui-monokai-button--active-backgroundColor + .item-simple-wrapper + border-color transparent + .item-simple-title + .item-simple-title-empty + .item-simple-title-icon + .item-simple-bottom-time + color $ui-monokai-text-color + .item-simple-bottom-tagList-item + background-color alpha(white, 10%) + color $ui-monokai-text-color + &:hover + // background-color alpha($ui-dark-button--active-backgroundColor, 60%) + color #c0392b + .item-simple-bottom-tagList-item + background-color alpha(#fff, 20%) + .item-simple-title + color $ui-dark-text-color + border-bottom $ui-dark-borderColor + .item-simple-right + float right + .item-simple-right-storageName + padding-left 4px + opacity 0.4 diff --git a/browser/components/RealtimeNotification.styl b/browser/components/RealtimeNotification.styl index 0f77acbb..0365d8c9 100644 --- a/browser/components/RealtimeNotification.styl +++ b/browser/components/RealtimeNotification.styl @@ -41,3 +41,14 @@ body[data-theme="solarized-dark"] background-color $ui-solarized-dark-button-backgroundColor &:hover color #5CB85C + +body[data-theme="monokai"] + .notification-area + background-color none + + .notification-link + color $ui-monokai-text-color + border none + background-color $ui-monokai-button-backgroundColor + &:hover + color #5CB85C \ No newline at end of file diff --git a/browser/components/SideNavFilter.styl b/browser/components/SideNavFilter.styl index 8a9a350d..c9dbd861 100644 --- a/browser/components/SideNavFilter.styl +++ b/browser/components/SideNavFilter.styl @@ -18,7 +18,7 @@ .iconWrap width 20px text-align center - + .counters float right color $ui-inactive-text-color @@ -68,10 +68,9 @@ .menu-button-label position fixed display inline-block - height 32px + height 36px left 44px padding 0 10px - margin-top -8px margin-left 0 overflow ellipsis z-index 10 @@ -222,4 +221,46 @@ body[data-theme="solarized-dark"] background-color $ui-solarized-dark-button-backgroundColor color $ui-solarized-dark-text-color .menu-button-label - color $ui-solarized-dark-text-color \ No newline at end of file + color $ui-solarized-dark-text-color + +body[data-theme="monokai"] + .menu-button + &:active + background-color $ui-monokai-noteList-backgroundColor + color $ui-monokai-text-color + &:hover + background-color $ui-monokai-button-backgroundColor + color $ui-monokai-text-color + + .menu-button--active + color $ui-monokai-text-color + background-color $ui-monokai-button-backgroundColor + .menu-button-label + color $ui-monokai-text-color + &:hover + background-color $ui-monokai-button-backgroundColor + color $ui-monokai-text-color + .menu-button-label + color $ui-monokai-text-color + + .menu-button-star--active + color $ui-monokai-text-color + background-color $ui-monokai-button-backgroundColor + .menu-button-label + color $ui-monokai-text-color + &:hover + background-color $ui-monokai-button-backgroundColor + color $ui-monokai-text-color + .menu-button-label + color $ui-monokai-text-color + + .menu-button-trash--active + color $ui-monokai-text-color + background-color $ui-monokai-button-backgroundColor + .menu-button-label + color $ui-monokai-text-color + &:hover + background-color $ui-monokai-button-backgroundColor + color $ui-monokai-text-color + .menu-button-label + color $ui-monokai-text-color \ No newline at end of file diff --git a/browser/components/SnippetTab.js b/browser/components/SnippetTab.js index 89f5a5bc..c030351f 100644 --- a/browser/components/SnippetTab.js +++ b/browser/components/SnippetTab.js @@ -55,10 +55,10 @@ class SnippetTab extends React.Component { this.handleRename() break case 27: - this.setState({ - name: this.props.snippet.name, + this.setState((prevState, props) => ({ + name: props.snippet.name, isRenaming: false - }) + })) break } } diff --git a/browser/components/StorageItem.styl b/browser/components/StorageItem.styl index 842f8d66..0a1b4525 100644 --- a/browser/components/StorageItem.styl +++ b/browser/components/StorageItem.styl @@ -58,8 +58,8 @@ opacity 0 border-top-right-radius 2px border-bottom-right-radius 2px - height 26px - line-height 26px + height 34px + line-height 32px .folderList-item:hover, .folderList-item--active:hover .folderList-item-tooltip @@ -138,3 +138,22 @@ body[data-theme="solarized-dark"] &:hover color $ui-solarized-dark-text-color background-color $ui-solarized-dark-button-backgroundColor + +body[data-theme="monokai"] + .folderList-item + &:hover + background-color $ui-monokai-button-backgroundColor + color $ui-monokai-text-color + &:active + color $ui-monokai-text-color + background-color $ui-monokai-button-backgroundColor + + .folderList-item--active + @extend .folderList-item + color $ui-monokai-text-color + background-color $ui-monokai-button-backgroundColor + &:active + background-color $ui-monokai-button-backgroundColor + &:hover + color $ui-monokai-text-color + background-color $ui-monokai-button-backgroundColor \ No newline at end of file diff --git a/browser/components/TodoListPercentage.styl b/browser/components/TodoListPercentage.styl index 329663f9..6116cd58 100644 --- a/browser/components/TodoListPercentage.styl +++ b/browser/components/TodoListPercentage.styl @@ -47,5 +47,15 @@ body[data-theme="solarized-dark"] .progressBar background-color: #2aa198 + .percentageText + color #fdf6e3 + +body[data-theme="monokai"] + .percentageBar + background-color #f92672 + + .progressBar + background-color: #373831 + .percentageText color #fdf6e3 \ No newline at end of file diff --git a/browser/components/markdown.styl b/browser/components/markdown.styl index cc6d7d92..32dbda73 100644 --- a/browser/components/markdown.styl +++ b/browser/components/markdown.styl @@ -199,7 +199,6 @@ ol &>li>ul, &>li>ol margin 0 code - color #CC305F padding 0.2em 0.4em background-color #f7f7f7 border-radius 3px @@ -371,3 +370,30 @@ body[data-theme="solarized-dark"] border-color themeSolarizedDarkTableBorder &:last-child border-right solid 1px themeSolarizedDarkTableBorder + +themeMonokaiTableOdd = $ui-monokai-noteDetail-backgroundColor +themeMonokaiTableEven = darken($ui-monokai-noteDetail-backgroundColor, 10%) +themeMonokaiTableHead = themeMonokaiTableEven +themeMonokaiTableBorder = themeDarkBorder + +body[data-theme="monokai"] + color $ui-monokai-text-color + border-color themeDarkBorder + background-color $ui-monokai-noteDetail-backgroundColor + table + thead + tr + background-color themeMonokaiTableHead + th + border-color themeMonokaiTableBorder + &:last-child + border-right solid 1px themeMonokaiTableBorder + tbody + tr:nth-child(2n + 1) + background-color themeMonokaiTableOdd + tr:nth-child(2n) + background-color themeMonokaiTableEven + td + border-color themeMonokaiTableBorder + &:last-child + border-right solid 1px themeMonokaiTableBorder \ No newline at end of file diff --git a/browser/lib/convertModeName.js b/browser/lib/convertModeName.js new file mode 100644 index 00000000..b0431059 --- /dev/null +++ b/browser/lib/convertModeName.js @@ -0,0 +1,14 @@ +export default function convertModeName (name) { + switch (name) { + case 'ejs': + return 'Embedded Javascript' + case 'html_ruby': + return 'Embedded Ruby' + case 'objectivec': + return 'Objective C' + case 'text': + return 'Plain Text' + default: + return name + } +} diff --git a/browser/lib/markdown.js b/browser/lib/markdown.js index 1ef488a7..b9e1a3eb 100644 --- a/browser/lib/markdown.js +++ b/browser/lib/markdown.js @@ -25,7 +25,7 @@ class Markdown { linkify: true, html: true, xhtmlOut: true, - breaks: true, + breaks: config.preview.breaks, highlight: function (str, lang) { const delimiter = ':' const langInfo = lang.split(delimiter) @@ -145,11 +145,13 @@ 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 `http://www.plantuml.com/plantuml/svg/${zippedCode}` + return `${serverAddress}/${zippedCode}` } }) diff --git a/browser/lib/utils.js b/browser/lib/utils.js index f67ca377..441cfbc7 100644 --- a/browser/lib/utils.js +++ b/browser/lib/utils.js @@ -54,7 +54,25 @@ export function escapeHtmlCharacters (text) { : html } +export function isObjectEqual (a, b) { + const aProps = Object.getOwnPropertyNames(a) + const bProps = Object.getOwnPropertyNames(b) + + if (aProps.length !== bProps.length) { + return false + } + + for (var i = 0; i < aProps.length; i++) { + const propName = aProps[i] + if (a[propName] !== b[propName]) { + return false + } + } + return true +} + export default { lastFindInArray, - escapeHtmlCharacters + escapeHtmlCharacters, + isObjectEqual } diff --git a/browser/main/Detail/Detail.styl b/browser/main/Detail/Detail.styl index d4c4100c..49a634f3 100644 --- a/browser/main/Detail/Detail.styl +++ b/browser/main/Detail/Detail.styl @@ -30,3 +30,10 @@ body[data-theme="solarized-dark"] border-left 1px solid $ui-solarized-dark-borderColor .empty-message color $ui-solarized-dark-text-color + +body[data-theme="monokai"] + .root + background-color $ui-monokai-noteDetail-backgroundColor + border-left 1px solid $ui-monokai-borderColor + .empty-message + color $ui-monokai-text-color diff --git a/browser/main/Detail/FolderSelect.styl b/browser/main/Detail/FolderSelect.styl index 31930fe6..cfdc2734 100644 --- a/browser/main/Detail/FolderSelect.styl +++ b/browser/main/Detail/FolderSelect.styl @@ -133,3 +133,29 @@ body[data-theme="dark"] color $ui-dark-button--active-color .search-optionList-item-name-surfix color $ui-dark-inactive-text-color + +body[data-theme="monokai"] + .root + color $ui-dark-text-color + &:hover + color white + background-color $ui-monokai-button--hover-backgroundColor + border-color $ui-monokai-borderColor + + .search-optionList + color white + border-color $ui-monokai-borderColor + background-color $ui-monokai-button-backgroundColor + + .search-optionList-item + &:hover + background-color lighten($ui-monokai-button--hover-backgroundColor, 15%) + + .search-optionList-item--active + background-color $ui-monokai-button--active-backgroundColor + color $ui-monokai-button--active-color + &:hover + background-color $ui-monokai-button--active-backgroundColor + color $ui-monokai-button--active-color + .search-optionList-item-name-surfix + color $ui-monokai-inactive-text-color diff --git a/browser/main/Detail/InfoPanel.styl b/browser/main/Detail/InfoPanel.styl index d90dea49..480441bd 100644 --- a/browser/main/Detail/InfoPanel.styl +++ b/browser/main/Detail/InfoPanel.styl @@ -215,3 +215,43 @@ body[data-theme="solarized-dark"] color $ui-dark-inactive-text-color &:hover color $ui-solarized-ark-text-color + +body[data-theme="monokai"] + .control-infoButton-panel + background-color $ui-monokai-noteList-backgroundColor + + .control-infoButton-panel-trash + background-color $ui-monokai-noteList-backgroundColor + + .modification-date + color $ui-monokai-text-color + + .modification-date-desc + color $ui-inactive-text-color + + .infoPanel-defaul-count + color $ui-monokai-text-color + + .infoPanel-sub-count + color $ui-inactive-text-color + + .infoPanel-default + color $ui-monokai-text-color + + .infoPanel-sub + color $ui-inactive-text-color + + .infoPanel-noteLink + background-color alpha($ui-monokai-borderColor, 20%) + color $ui-monokai-text-color + + [id=export-wrap] + button + color $ui-dark-inactive-text-color + &:hover + background-color alpha($ui-monokai-borderColor, 20%) + color $ui-monokai-text-color + p + color $ui-dark-inactive-text-color + &:hover + color $ui-monokai-text-color diff --git a/browser/main/Detail/MarkdownNoteDetail.js b/browser/main/Detail/MarkdownNoteDetail.js index 72f832e3..a8fc938b 100755 --- a/browser/main/Detail/MarkdownNoteDetail.js +++ b/browser/main/Detail/MarkdownNoteDetail.js @@ -55,10 +55,14 @@ class MarkdownNoteDetail extends React.Component { componentDidMount () { ee.on('topbar:togglelockbutton', this.toggleLockButton) + ee.on('topbar:togglemodebutton', () => { + const reversedType = this.state.editorType === 'SPLIT' ? 'EDITOR_PREVIEW' : 'SPLIT' + this.handleSwitchMode(reversedType) + }) } componentWillReceiveProps (nextProps) { - if (nextProps.note.key !== this.props.note.key && !this.isMovingNote) { + if (nextProps.note.key !== this.props.note.key && !this.state.isMovingNote) { if (this.saveQueue != null) this.saveNow() this.setState({ note: Object.assign({}, nextProps.note) diff --git a/browser/main/Detail/MarkdownNoteDetail.styl b/browser/main/Detail/MarkdownNoteDetail.styl index ad20f0f2..b27dc80e 100644 --- a/browser/main/Detail/MarkdownNoteDetail.styl +++ b/browser/main/Detail/MarkdownNoteDetail.styl @@ -71,3 +71,8 @@ body[data-theme="solarized-dark"] .root border-left 1px solid $ui-solarized-dark-borderColor background-color $ui-solarized-dark-noteDetail-backgroundColor + +body[data-theme="monokai"] + .root + border-left 1px solid $ui-monokai-borderColor + background-color $ui-monokai-noteDetail-backgroundColor diff --git a/browser/main/Detail/NoteDetailInfo.styl b/browser/main/Detail/NoteDetailInfo.styl index bc3c9462..8d454203 100644 --- a/browser/main/Detail/NoteDetailInfo.styl +++ b/browser/main/Detail/NoteDetailInfo.styl @@ -98,3 +98,7 @@ body[data-theme="solarized-dark"] border-color $ui-solarized-dark-borderColor background-color $ui-solarized-dark-noteDetail-backgroundColor +body[data-theme="monokai"] + .info + border-color $ui-monokai-borderColor + background-color $ui-monokai-noteDetail-backgroundColor \ No newline at end of file diff --git a/browser/main/Detail/SnippetNoteDetail.js b/browser/main/Detail/SnippetNoteDetail.js index 411027d5..c65f1425 100644 --- a/browser/main/Detail/SnippetNoteDetail.js +++ b/browser/main/Detail/SnippetNoteDetail.js @@ -18,6 +18,7 @@ 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 TrashButton from './TrashButton' import RestoreButton from './RestoreButton' @@ -29,21 +30,6 @@ import { formatDate } from 'browser/lib/date-formatter' import i18n from 'browser/lib/i18n' import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote' -function pass (name) { - switch (name) { - case 'ejs': - return 'Embedded Javascript' - case 'html_ruby': - return 'Embedded Ruby' - case 'objectivec': - return 'Objective C' - case 'text': - return 'Plain Text' - default: - return name - } -} - const electron = require('electron') const { remote } = electron const { Menu, MenuItem, dialog } = remote @@ -82,7 +68,7 @@ class SnippetNoteDetail extends React.Component { } componentWillReceiveProps (nextProps) { - if (nextProps.note.key !== this.props.note.key && !this.isMovingNote) { + if (nextProps.note.key !== this.props.note.key && !this.state.isMovingNote) { if (this.saveQueue != null) this.saveNow() const nextNote = Object.assign({ description: '' @@ -382,11 +368,11 @@ class SnippetNoteDetail extends React.Component { name: mode }) } - this.setState({note: Object.assign(this.state.note, {snippets: snippets})}) + this.setState(state => ({note: Object.assign(state.note, {snippets: snippets})})) - this.setState({ - note: this.state.note - }, () => { + this.setState(state => ({ + note: state.note + }), () => { this.save() }) } @@ -395,11 +381,11 @@ class SnippetNoteDetail extends React.Component { return (e) => { const snippets = this.state.note.snippets.slice() snippets[index].mode = name - this.setState({note: Object.assign(this.state.note, {snippets: snippets})}) + this.setState(state => ({note: Object.assign(state.note, {snippets: snippets})})) - this.setState({ - note: this.state.note - }, () => { + this.setState(state => ({ + note: state.note + }), () => { this.save() }) @@ -413,10 +399,10 @@ class SnippetNoteDetail extends React.Component { return (e) => { const snippets = this.state.note.snippets.slice() snippets[index].content = this.refs['code-' + index].value - this.setState({note: Object.assign(this.state.note, {snippets: snippets})}) - this.setState({ - note: this.state.note - }, () => { + this.setState(state => ({note: Object.assign(state.note, {snippets: snippets})})) + this.setState(state => ({ + note: state.note + }), () => { this.save() }) } @@ -611,17 +597,17 @@ class SnippetNoteDetail extends React.Component { } jumpNextTab () { - this.setState({ - snippetIndex: (this.state.snippetIndex + 1) % this.state.note.snippets.length - }, () => { + this.setState(state => ({ + snippetIndex: (state.snippetIndex + 1) % state.note.snippets.length + }), () => { this.focusEditor() }) } jumpPrevTab () { - this.setState({ - snippetIndex: (this.state.snippetIndex - 1 + this.state.note.snippets.length) % this.state.note.snippets.length - }, () => { + this.setState(state => ({ + snippetIndex: (state.snippetIndex - 1 + state.note.snippets.length) % state.note.snippets.length + }), () => { this.focusEditor() }) } @@ -677,7 +663,7 @@ class SnippetNoteDetail extends React.Component { const viewList = note.snippets.map((snippet, index) => { const isActive = this.state.snippetIndex === index - let syntax = CodeMirror.findModeByName(pass(snippet.mode)) + let syntax = CodeMirror.findModeByName(convertModeName(snippet.mode)) if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text') return
{ + value.pop() + }) } reset () { @@ -96,15 +89,22 @@ class TagSelect extends React.Component { } handleTagRemoveButtonClick (tag) { - return (e) => { - let { value } = this.props - + this.removeTagByCallback((value, tag) => { value.splice(value.indexOf(tag), 1) - value = _.uniq(value) + }, tag) + } - this.value = value - this.props.onChange() - } + removeTagByCallback (callback, tag = null) { + let { value } = this.props + + value = _.isArray(value) + ? value.slice() + : [] + callback(value, tag) + value = _.uniq(value) + + this.value = value + this.props.onChange() } render () { @@ -118,7 +118,7 @@ class TagSelect extends React.Component { > #{tag} diff --git a/browser/main/Detail/TagSelect.styl b/browser/main/Detail/TagSelect.styl index 18d4d2e0..0ff4c6a3 100644 --- a/browser/main/Detail/TagSelect.styl +++ b/browser/main/Detail/TagSelect.styl @@ -81,4 +81,20 @@ body[data-theme="solarized-dark"] .newTag border-color none background-color transparent - color $ui-solarized-dark-text-color \ No newline at end of file + color $ui-solarized-dark-text-color + +body[data-theme="monokai"] + .tag + background-color $ui-monokai-button-backgroundColor + + .tag-removeButton + border-color $ui-button--focus-borderColor + background-color transparent + + .tag-label + color $ui-monokai-text-color + + .newTag + border-color none + background-color transparent + color $ui-monokai-text-color diff --git a/browser/main/Detail/ToggleModeButton.styl b/browser/main/Detail/ToggleModeButton.styl index 185a780c..2e7ab5fa 100644 --- a/browser/main/Detail/ToggleModeButton.styl +++ b/browser/main/Detail/ToggleModeButton.styl @@ -56,3 +56,10 @@ body[data-theme="solarized-dark"] .active background-color #1EC38B box-shadow 2px 0px 7px #222222 + +body[data-theme="monokai"] + .control-toggleModeButton + background-color #272822 + .active + background-color #1EC38B + box-shadow 2px 0px 7px #222222 diff --git a/browser/main/Main.js b/browser/main/Main.js index 9f1c06e7..69b16bc7 100644 --- a/browser/main/Main.js +++ b/browser/main/Main.js @@ -16,6 +16,7 @@ import { hashHistory } from 'react-router' 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' const path = require('path') const electron = require('electron') const { remote } = electron @@ -144,7 +145,8 @@ class Main extends React.Component { const supportedThemes = [ 'dark', 'white', - 'solarized-dark' + 'solarized-dark', + 'monokai' ] if (supportedThemes.indexOf(config.ui.theme) !== -1) { @@ -158,7 +160,7 @@ class Main extends React.Component { } else { i18n.setLocale('en') } - + applyShortcuts() // Reload all data dataApi.init() .then((data) => { diff --git a/browser/main/NewNoteButton/NewNoteButton.styl b/browser/main/NewNoteButton/NewNoteButton.styl index 81ff7e8d..e8e4b5f0 100644 --- a/browser/main/NewNoteButton/NewNoteButton.styl +++ b/browser/main/NewNoteButton/NewNoteButton.styl @@ -74,4 +74,8 @@ body[data-theme="dark"] body[data-theme="solarized-dark"] .root, .root--expanded - background-color $ui-solarized-dark-noteList-backgroundColor \ No newline at end of file + background-color $ui-solarized-dark-noteList-backgroundColor + +body[data-theme="monokai"] + .root, .root--expanded + background-color $ui-monokai-noteList-backgroundColor diff --git a/browser/main/NoteList/NoteList.styl b/browser/main/NoteList/NoteList.styl index 312f5143..ea261208 100644 --- a/browser/main/NoteList/NoteList.styl +++ b/browser/main/NoteList/NoteList.styl @@ -113,4 +113,28 @@ body[data-theme="solarized-dark"] .control-button--active color $ui-solarized-dark-text-color &:active - color $ui-solarized-dark-text-color \ No newline at end of file + color $ui-solarized-dark-text-color + +body[data-theme="monokai"] + .root + border-color $ui-monokai-borderColor + background-color $ui-monokai-noteList-backgroundColor + + .control + background-color $ui-monokai-noteList-backgroundColor + border-color $ui-monokai-borderColor + + .control-sortBy-select + &:hover + transition 0.2s + color $ui-monokai-text-color + + .control-button + color $ui-monokai-inactive-text-color + &:hover + color $ui-monokai-text-color + + .control-button--active + color $ui-monokai-text-color + &:active + color $ui-monokai-text-color diff --git a/browser/main/NoteList/index.js b/browser/main/NoteList/index.js index e8c09f65..d6b7f846 100644 --- a/browser/main/NoteList/index.js +++ b/browser/main/NoteList/index.js @@ -7,6 +7,7 @@ import moment from 'moment' import _ from 'lodash' import ee from 'browser/main/lib/eventEmitter' import dataApi from 'browser/main/lib/dataApi' +import attachmentManagement from 'browser/main/lib/dataApi/attachmentManagement' import ConfigManager from 'browser/main/lib/ConfigManager' import NoteItem from 'browser/components/NoteItem' import NoteItemSimple from 'browser/components/NoteItemSimple' @@ -455,12 +456,19 @@ class NoteList extends React.Component { } handleDragStart (e, note) { - const { selectedNoteKeys } = this.state + let { selectedNoteKeys } = this.state + const noteKey = getNoteKey(note) + + if (!selectedNoteKeys.includes(noteKey)) { + selectedNoteKeys = [] + selectedNoteKeys.push(noteKey) + } + const notes = this.notes.map((note) => Object.assign({}, note)) const selectedNotes = findNotesByKeys(notes, selectedNoteKeys) const noteData = JSON.stringify(selectedNotes) e.dataTransfer.setData('note', noteData) - this.setState({ selectedNoteKeys: [] }) + this.selectNextNote() } handleNoteContextMenu (e, uniqueKey) { @@ -655,6 +663,10 @@ class NoteList extends React.Component { title: firstNote.title + ' ' + i18n.__('copy'), content: firstNote.content }) + .then((note) => { + attachmentManagement.cloneAttachments(firstNote, note) + return note + }) .then((note) => { dispatch({ type: 'UPDATE_NOTE', diff --git a/browser/main/SideNav/SideNav.styl b/browser/main/SideNav/SideNav.styl index 666ae0cd..ecab70d0 100644 --- a/browser/main/SideNav/SideNav.styl +++ b/browser/main/SideNav/SideNav.styl @@ -117,3 +117,8 @@ body[data-theme="solarized-dark"] .root, .root--folded background-color $ui-solarized-dark-backgroundColor border-right 1px solid $ui-solarized-dark-borderColor + +body[data-theme="monokai"] + .root, .root--folded + background-color $ui-monokai-backgroundColor + border-right 1px solid $ui-monokai-borderColor diff --git a/browser/main/SideNav/StorageItem.js b/browser/main/SideNav/StorageItem.js index a4d74c30..93e9157f 100644 --- a/browser/main/SideNav/StorageItem.js +++ b/browser/main/SideNav/StorageItem.js @@ -14,6 +14,8 @@ import i18n from 'browser/lib/i18n' const { remote } = require('electron') const { Menu, dialog } = remote +const escapeStringRegexp = require('escape-string-regexp') +const path = require('path') class StorageItem extends React.Component { constructor (props) { @@ -201,7 +203,7 @@ class StorageItem extends React.Component { createdNoteData.forEach((newNote) => { dispatch({ type: 'MOVE_NOTE', - originNote: noteData.find((note) => note.content === newNote.content), + originNote: noteData.find((note) => note.content === newNote.oldContent), note: newNote }) }) @@ -223,7 +225,8 @@ class StorageItem extends React.Component { const { folderNoteMap, trashedSet } = data const SortableStorageItemChild = SortableElement(StorageItemChild) const folderList = storage.folders.map((folder, index) => { - const isActive = !!(location.pathname.match(new RegExp('\/storages\/' + storage.key + '\/folders\/' + folder.key))) + let folderRegex = new RegExp(escapeStringRegexp(path.sep) + 'storages' + escapeStringRegexp(path.sep) + storage.key + escapeStringRegexp(path.sep) + 'folders' + escapeStringRegexp(path.sep) + folder.key) + const isActive = !!(location.pathname.match(folderRegex)) const noteSet = folderNoteMap.get(storage.key + '-' + folder.key) let noteCount = 0 @@ -253,7 +256,7 @@ class StorageItem extends React.Component { ) }) - const isActive = location.pathname.match(new RegExp('\/storages\/' + storage.key + '$')) + const isActive = location.pathname.match(new RegExp(escapeStringRegexp(path.sep) + 'storages' + escapeStringRegexp(path.sep) + storage.key + '$')) return (
({ name, size: tag.size, related: relatedTags.has(name) }) - ), ['name']) + ), ['name']).filter( + tag => tag.size > 0 + ) if (config.sortTagsBy === 'COUNTER') { tagList = _.sortBy(tagList, item => (0 - item.size)) } diff --git a/browser/main/StatusBar/StatusBar.styl b/browser/main/StatusBar/StatusBar.styl index 9f189fec..52cc4b02 100644 --- a/browser/main/StatusBar/StatusBar.styl +++ b/browser/main/StatusBar/StatusBar.styl @@ -69,3 +69,14 @@ body[data-theme="dark"] navDarkButtonColor() border-color $ui-dark-borderColor border-left 1px solid $ui-dark-borderColor + +body[data-theme="monokai"] + navButtonColor() + .zoom + border-color $ui-dark-borderColor + color $ui-monokai-text-color + &:hover + transition 0.15s + color $ui-monokai-active-color + &:active + color $ui-monokai-active-color diff --git a/browser/main/TopBar/TopBar.styl b/browser/main/TopBar/TopBar.styl index 0956571f..7654f66f 100644 --- a/browser/main/TopBar/TopBar.styl +++ b/browser/main/TopBar/TopBar.styl @@ -234,3 +234,25 @@ body[data-theme="solarized-dark"] input background-color $ui-solarized-dark-noteList-backgroundColor color $ui-solarized-dark-text-color + +body[data-theme="monokai"] + .root, .root--expanded + background-color $ui-monokai-noteList-backgroundColor + + .control + border-color $ui-monokai-borderColor + .control-search + background-color $ui-monokai-noteList-backgroundColor + + .control-search-icon + absolute top bottom left + line-height 32px + width 35px + color $ui-monokai-inactive-text-color + background-color $ui-monokai-noteList-backgroundColor + + .control-search-input + background-color $ui-monokai-noteList-backgroundColor + input + background-color $ui-monokai-noteList-backgroundColor + color $ui-monokai-text-color diff --git a/browser/main/global.styl b/browser/main/global.styl index 613c7611..7025163f 100644 --- a/browser/main/global.styl +++ b/browser/main/global.styl @@ -134,4 +134,10 @@ body[data-theme="solarized-dark"] .sortableItemHelper color: $ui-solarized-dark-text-color +body[data-theme="monokai"] + .ModalBase + .modalBack + background-color $ui-monokai-backgroundColor + .sortableItemHelper + color: $ui-monokai-text-color diff --git a/browser/main/lib/ConfigManager.js b/browser/main/lib/ConfigManager.js index 3e1a2162..228692d6 100644 --- a/browser/main/lib/ConfigManager.js +++ b/browser/main/lib/ConfigManager.js @@ -1,6 +1,7 @@ import _ from 'lodash' import RcParser from 'browser/lib/RcParser' import i18n from 'browser/lib/i18n' +import ee from 'browser/main/lib/eventEmitter' const OSX = global.process.platform === 'darwin' const win = global.process.platform === 'win32' @@ -20,7 +21,8 @@ export const DEFAULT_CONFIG = { listStyle: 'DEFAULT', // 'DEFAULT', 'SMALL' amaEnabled: true, hotkey: { - toggleMain: OSX ? 'Cmd + Alt + L' : 'Super + Alt + E' + toggleMain: OSX ? 'Cmd + Alt + L' : 'Super + Alt + E', + toggleMode: OSX ? 'Cmd + M' : 'Ctrl + M' }, ui: { language: 'en', @@ -53,8 +55,10 @@ export const DEFAULT_CONFIG = { latexInlineClose: '$', latexBlockOpen: '$$', latexBlockClose: '$$', + plantUMLServerAddress: 'http://www.plantuml.com/plantuml', scrollPastEnd: false, smartQuotes: true, + breaks: true, sanitize: 'STRICT' // 'STRICT', 'ALLOW_STYLES', 'NONE' }, blog: { @@ -135,6 +139,8 @@ function set (updates) { document.body.setAttribute('data-theme', 'white') } else if (newConfig.ui.theme === 'solarized-dark') { document.body.setAttribute('data-theme', 'solarized-dark') + } else if (newConfig.ui.theme === 'monokai') { + document.body.setAttribute('data-theme', 'monokai') } else { document.body.setAttribute('data-theme', 'default') } @@ -163,6 +169,7 @@ function set (updates) { ipcRenderer.send('config-renew', { config: get() }) + ee.emit('config-renew') } function assignConfigValues (originalConfig, rcConfig) { diff --git a/browser/main/lib/dataApi/attachmentManagement.js b/browser/main/lib/dataApi/attachmentManagement.js index ac0986e1..893e03d1 100644 --- a/browser/main/lib/dataApi/attachmentManagement.js +++ b/browser/main/lib/dataApi/attachmentManagement.js @@ -3,6 +3,9 @@ const fs = require('fs') const path = require('path') const findStorage = require('browser/lib/findStorage') const mdurl = require('mdurl') +const fse = require('fs-extra') +const escapeStringRegexp = require('escape-string-regexp') +const sander = require('sander') const STORAGE_FOLDER_PLACEHOLDER = ':storage' const DESTINATION_FOLDER = 'attachments' @@ -39,7 +42,7 @@ function copyAttachment (sourceFilePath, storageKey, noteKey, useRandomName = tr const targetStorage = findStorage.findStorage(storageKey) - const inputFile = fs.createReadStream(sourceFilePath) + const inputFileStream = fs.createReadStream(sourceFilePath) let destinationName if (useRandomName) { destinationName = `${uniqueSlug()}${path.extname(sourceFilePath)}` @@ -49,8 +52,10 @@ function copyAttachment (sourceFilePath, storageKey, noteKey, useRandomName = tr const destinationDir = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey) createAttachmentDestinationFolder(targetStorage.path, noteKey) const outputFile = fs.createWriteStream(path.join(destinationDir, destinationName)) - inputFile.pipe(outputFile) - resolve(destinationName) + inputFileStream.pipe(outputFile) + inputFileStream.on('end', () => { + resolve(destinationName) + }) } catch (e) { return reject(e) } @@ -146,19 +151,176 @@ function handlePastImageEvent (codeEditor, storageKey, noteKey, dataTransferItem base64data = reader.result.replace(/^data:image\/png;base64,/, '') base64data += base64data.replace('+', ' ') const binaryData = new Buffer(base64data, 'base64').toString('binary') - fs.writeFile(imagePath, binaryData, 'binary') + fs.writeFileSync(imagePath, binaryData, 'binary') const imageMd = generateAttachmentMarkdown(imageName, imagePath, true) codeEditor.insertAttachmentMd(imageMd) } reader.readAsDataURL(blob) } +/** + * @description Returns all attachment paths of the given markdown + * @param {String} markdownContent content in which the attachment paths should be found + * @returns {String[]} Array of the relative paths (starting with :storage) of the attachments of the given markdown + */ +function getAttachmentsInContent (markdownContent) { + const preparedInput = markdownContent.replace(new RegExp(mdurl.encode(path.sep), 'g'), path.sep) + const regexp = new RegExp(STORAGE_FOLDER_PLACEHOLDER + escapeStringRegexp(path.sep) + '([a-zA-Z0-9]|-)+' + escapeStringRegexp(path.sep) + '[a-zA-Z0-9]+(\\.[a-zA-Z0-9]+)?', 'g') + return preparedInput.match(regexp) +} + +/** + * @description Returns an array of the absolute paths of the attachments referenced in the given markdown code + * @param {String} markdownContent content in which the attachment paths should be found + * @param {String} storagePath path of the current storage + * @returns {String[]} Absolute paths of the referenced attachments + */ +function getAbsolutePathsOfAttachmentsInContent (markdownContent, storagePath) { + const temp = getAttachmentsInContent(markdownContent) || [] + const result = [] + for (const relativePath of temp) { + result.push(relativePath.replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER, 'g'), path.join(storagePath, DESTINATION_FOLDER))) + } + return result +} + +/** + * @description Moves the attachments of the current note to the new location. + * Returns a modified version of the given content so that the links to the attachments point to the new note key. + * @param {String} oldPath Source of the note to be moved + * @param {String} newPath Destination of the note to be moved + * @param {String} noteKey Old note key + * @param {String} newNoteKey New note key + * @param {String} noteContent Content of the note to be moved + * @returns {String} Modified version of noteContent in which the paths of the attachments are fixed + */ +function moveAttachments (oldPath, newPath, noteKey, newNoteKey, noteContent) { + const src = path.join(oldPath, DESTINATION_FOLDER, noteKey) + const dest = path.join(newPath, DESTINATION_FOLDER, newNoteKey) + if (fse.existsSync(src)) { + fse.moveSync(src, dest) + } + return replaceNoteKeyWithNewNoteKey(noteContent, noteKey, newNoteKey) +} + +/** + * Modifies the given content so that in all attachment references the oldNoteKey is replaced by the new one + * @param noteContent content that should be modified + * @param oldNoteKey note key to be replaced + * @param newNoteKey note key serving as a replacement + * @returns {String} modified note content + */ +function replaceNoteKeyWithNewNoteKey (noteContent, oldNoteKey, newNoteKey) { + if (noteContent) { + return noteContent.replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER + escapeStringRegexp(path.sep) + oldNoteKey, 'g'), path.join(STORAGE_FOLDER_PLACEHOLDER, newNoteKey)) + } + return noteContent +} + +/** + * @description Deletes all :storage and noteKey references from the given input. + * @param input Input in which the references should be deleted + * @param noteKey Key of the current note + * @returns {String} Input without the references + */ +function removeStorageAndNoteReferences (input, noteKey) { + return input.replace(new RegExp(mdurl.encode(path.sep), 'g'), path.sep).replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER + escapeStringRegexp(path.sep) + noteKey, 'g'), DESTINATION_FOLDER) +} + +/** + * @description Deletes the attachment folder specified by the given storageKey and noteKey + * @param storageKey Key of the storage of the note to be deleted + * @param noteKey Key of the note to be deleted + */ +function deleteAttachmentFolder (storageKey, noteKey) { + const storagePath = findStorage.findStorage(storageKey) + const noteAttachmentPath = path.join(storagePath.path, DESTINATION_FOLDER, noteKey) + sander.rimrafSync(noteAttachmentPath) +} + +/** + * @description Deletes all attachments stored in the attachment folder of the give not that are not referenced in the markdownContent + * @param markdownContent Content of the note. All unreferenced notes will be deleted + * @param storageKey StorageKey of the current note. Is used to determine the belonging attachment folder. + * @param noteKey NoteKey of the current note. Is used to determine the belonging attachment folder. + */ +function deleteAttachmentsNotPresentInNote (markdownContent, storageKey, noteKey) { + const targetStorage = findStorage.findStorage(storageKey) + const attachmentFolder = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey) + const attachmentsInNote = getAttachmentsInContent(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)) { + fs.readdir(attachmentFolder, (err, files) => { + if (err) { + console.error("Error reading directory '" + attachmentFolder + "'. Error:") + console.error(err) + return + } + files.forEach(file => { + if (!attachmentsInNoteOnlyFileNames.includes(file)) { + const absolutePathOfFile = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey, file) + fs.unlink(absolutePathOfFile, (err) => { + if (err) { + console.error("Could not delete '%s'", absolutePathOfFile) + console.error(err) + return + } + console.info("File '" + absolutePathOfFile + "' deleted because it was not included in the content of the note") + }) + } + }) + }) + } else { + console.info("Attachment folder ('" + attachmentFolder + "') did not exist..") + } +} + +/** + * 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. + * @param oldNote Note that is being cloned + * @param newNote Clone of the note + */ +function cloneAttachments (oldNote, newNote) { + if (newNote.type === 'MARKDOWN_NOTE') { + const oldStorage = findStorage.findStorage(oldNote.storage) + const newStorage = findStorage.findStorage(newNote.storage) + const attachmentsPaths = getAbsolutePathsOfAttachmentsInContent(oldNote.content, oldStorage.path) || [] + + const destinationFolder = path.join(newStorage.path, DESTINATION_FOLDER, newNote.key) + if (!sander.existsSync(destinationFolder)) { + sander.mkdirSync(destinationFolder) + } + + for (const attachment of attachmentsPaths) { + const destination = path.join(newStorage.path, DESTINATION_FOLDER, newNote.key, path.basename(attachment)) + sander.copyFileSync(attachment).to(destination) + } + newNote.content = replaceNoteKeyWithNewNoteKey(newNote.content, oldNote.key, newNote.key) + } else { + console.debug('Cloning of the attachment was skipped since it only works for MARKDOWN_NOTEs') + } +} + module.exports = { copyAttachment, fixLocalURLS, generateAttachmentMarkdown, handleAttachmentDrop, handlePastImageEvent, + getAttachmentsInContent, + getAbsolutePathsOfAttachmentsInContent, + removeStorageAndNoteReferences, + deleteAttachmentFolder, + deleteAttachmentsNotPresentInNote, + moveAttachments, + cloneAttachments, STORAGE_FOLDER_PLACEHOLDER, DESTINATION_FOLDER } diff --git a/browser/main/lib/dataApi/copyImage.js b/browser/main/lib/dataApi/copyImage.js deleted file mode 100644 index 24053bdd..00000000 --- a/browser/main/lib/dataApi/copyImage.js +++ /dev/null @@ -1,38 +0,0 @@ -const fs = require('fs') -const path = require('path') -const { findStorage } = require('browser/lib/findStorage') - -// TODO: ehhc: delete this - -/** - * @description Copy an image and return the path. - * @param {String} filePath - * @param {String} storageKey - * @param {Boolean} rename create new filename or leave the old one - * @return {Promise} an image path - */ -function copyImage (filePath, storageKey, rename = true) { - return new Promise((resolve, reject) => { - try { - const targetStorage = findStorage(storageKey) - - const inputImage = fs.createReadStream(filePath) - const imageExt = path.extname(filePath) - const imageName = rename ? Math.random().toString(36).slice(-16) : path.basename(filePath, imageExt) - const basename = `${imageName}${imageExt}` - const imageDir = path.join(targetStorage.path, 'images') - if (!fs.existsSync(imageDir)) fs.mkdirSync(imageDir) - const outputImage = fs.createWriteStream(path.join(imageDir, basename)) - outputImage.on('error', reject) - inputImage.on('error', reject) - inputImage.on('end', () => { - resolve(basename) - }) - inputImage.pipe(outputImage) - } catch (e) { - return reject(e) - } - }) -} - -module.exports = copyImage diff --git a/browser/main/lib/dataApi/deleteFolder.js b/browser/main/lib/dataApi/deleteFolder.js index 908677e1..0c7486f5 100644 --- a/browser/main/lib/dataApi/deleteFolder.js +++ b/browser/main/lib/dataApi/deleteFolder.js @@ -5,6 +5,7 @@ const resolveStorageNotes = require('./resolveStorageNotes') const CSON = require('@rokt33r/season') const sander = require('sander') const { findStorage } = require('browser/lib/findStorage') +const deleteSingleNote = require('./deleteNote') /** * @param {String} storageKey @@ -49,11 +50,7 @@ function deleteFolder (storageKey, folderKey) { const deleteAllNotes = targetNotes .map(function deleteNote (note) { - const notePath = path.join(storage.path, 'notes', note.key + '.cson') - return sander.unlink(notePath) - .catch(function (err) { - console.warn('Failed to delete', notePath, err) - }) + return deleteSingleNote(storageKey, note.key) }) return Promise.all(deleteAllNotes) .then(() => storage) diff --git a/browser/main/lib/dataApi/deleteNote.js b/browser/main/lib/dataApi/deleteNote.js index 49498a30..46ec2b55 100644 --- a/browser/main/lib/dataApi/deleteNote.js +++ b/browser/main/lib/dataApi/deleteNote.js @@ -1,6 +1,7 @@ const resolveStorageData = require('./resolveStorageData') const path = require('path') const sander = require('sander') +const attachmentManagement = require('./attachmentManagement') const { findStorage } = require('browser/lib/findStorage') function deleteNote (storageKey, noteKey) { @@ -25,6 +26,10 @@ function deleteNote (storageKey, noteKey) { storageKey } }) + .then(function deleteAttachments (storageInfo) { + attachmentManagement.deleteAttachmentFolder(storageInfo.storageKey, storageInfo.noteKey) + return storageInfo + }) } module.exports = deleteNote diff --git a/browser/main/lib/dataApi/exportNote.js b/browser/main/lib/dataApi/exportNote.js index 71f7d017..e4fec5f4 100755 --- a/browser/main/lib/dataApi/exportNote.js +++ b/browser/main/lib/dataApi/exportNote.js @@ -1,14 +1,9 @@ import copyFile from 'browser/main/lib/dataApi/copyFile' import { findStorage } from 'browser/lib/findStorage' -import filenamify from 'filenamify' const fs = require('fs') const path = require('path') -const LOCAL_STORED_REGEX = /!\[(.*?)]\(\s*?\/:storage\/(.*\.\S*?)\)/gi -// TODO: ehhc: check this -> attachmentManagement -const IMAGES_FOLDER_NAME = 'images' - /** * Export note together with images * @@ -29,21 +24,7 @@ function exportNote (storageKey, noteContent, targetPath, outputFormatter) { throw new Error('Storage path is not found') } - let exportedData = noteContent.replace(LOCAL_STORED_REGEX, (match, dstFilename, srcFilename) => { - dstFilename = filenamify(dstFilename, {replacement: '_'}) - if (!path.extname(dstFilename)) { - dstFilename += path.extname(srcFilename) - } - - const dstRelativePath = path.join(IMAGES_FOLDER_NAME, dstFilename) - - exportTasks.push({ - src: path.join(IMAGES_FOLDER_NAME, srcFilename), - dst: dstRelativePath - }) - - return `![${dstFilename}](${dstRelativePath})` - }) + let exportedData = noteContent if (outputFormatter) { exportedData = outputFormatter(exportedData, exportTasks) diff --git a/browser/main/lib/dataApi/moveNote.js b/browser/main/lib/dataApi/moveNote.js index cffb5c53..2d306cdf 100644 --- a/browser/main/lib/dataApi/moveNote.js +++ b/browser/main/lib/dataApi/moveNote.js @@ -6,7 +6,7 @@ const CSON = require('@rokt33r/season') const keygen = require('browser/lib/keygen') const sander = require('sander') const { findStorage } = require('browser/lib/findStorage') -const copyImage = require('./copyImage') +const attachmentManagement = require('./attachmentManagement') function moveNote (storageKey, noteKey, newStorageKey, newFolderKey) { let oldStorage, newStorage @@ -64,35 +64,20 @@ function moveNote (storageKey, noteKey, newStorageKey, newFolderKey) { noteData.key = newNoteKey noteData.storage = newStorageKey noteData.updatedAt = new Date() + noteData.oldContent = noteData.content return noteData }) - .then(function moveImages (noteData) { - if (oldStorage.path === newStorage.path) return noteData - - const searchImagesRegex = /!\[.*?]\(\s*?\/:storage\/(.*\.\S*?)\)/gi - let match = searchImagesRegex.exec(noteData.content) - - const moveTasks = [] - while (match != null) { - const [, filename] = match - const oldPath = path.join(oldStorage.path, 'images', filename) - // TODO: ehhc: attachmentManagement - moveTasks.push( - copyImage(oldPath, noteData.storage, false) - .then(() => { - fs.unlinkSync(oldPath) - }) - ) - - // find next occurence - match = searchImagesRegex.exec(noteData.content) + .then(function moveAttachments (noteData) { + if (oldStorage.path === newStorage.path) { + return noteData } - return Promise.all(moveTasks).then(() => noteData) + noteData.content = attachmentManagement.moveAttachments(oldStorage.path, newStorage.path, noteKey, newNoteKey, noteData.content) + return noteData }) .then(function writeAndReturn (noteData) { - CSON.writeFileSync(path.join(newStorage.path, 'notes', noteData.key + '.cson'), _.omit(noteData, ['key', 'storage'])) + CSON.writeFileSync(path.join(newStorage.path, 'notes', noteData.key + '.cson'), _.omit(noteData, ['key', 'storage', 'oldContent'])) return noteData }) .then(function deleteOldNote (data) { diff --git a/browser/main/lib/shortcut.js b/browser/main/lib/shortcut.js new file mode 100644 index 00000000..a6f33196 --- /dev/null +++ b/browser/main/lib/shortcut.js @@ -0,0 +1,7 @@ +import ee from 'browser/main/lib/eventEmitter' + +module.exports = { + 'toggleMode': () => { + ee.emit('topbar:togglemodebutton') + } +} diff --git a/browser/main/lib/shortcutManager.js b/browser/main/lib/shortcutManager.js new file mode 100644 index 00000000..2b937aea --- /dev/null +++ b/browser/main/lib/shortcutManager.js @@ -0,0 +1,40 @@ +import Mousetrap from 'mousetrap' +import CM from 'browser/main/lib/ConfigManager' +import ee from 'browser/main/lib/eventEmitter' +import { isObjectEqual } from 'browser/lib/utils' +require('mousetrap-global-bind') +const functions = require('./shortcut') + +let shortcuts = CM.get().hotkey + +ee.on('config-renew', function () { + // only update if hotkey changed ! + const newHotkey = CM.get().hotkey + if (!isObjectEqual(newHotkey, shortcuts)) { + updateShortcut(newHotkey) + } +}) + +function updateShortcut (newHotkey) { + Mousetrap.reset() + shortcuts = newHotkey + applyShortcuts(newHotkey) +} + +function formatShortcut (shortcut) { + return shortcut.toLowerCase().replace(/ /g, '') +} + +function applyShortcuts (shortcuts) { + for (const shortcut in shortcuts) { + const toggler = formatShortcut(shortcuts[shortcut]) + // only bind if the function for that shortcut exists + if (functions[shortcut]) { + Mousetrap.bindGlobal(toggler, functions[shortcut]) + } + } +} + +applyShortcuts(CM.get().hotkey) + +module.exports = applyShortcuts diff --git a/browser/main/modals/CreateFolderModal.styl b/browser/main/modals/CreateFolderModal.styl index 45f2e852..1b96e123 100644 --- a/browser/main/modals/CreateFolderModal.styl +++ b/browser/main/modals/CreateFolderModal.styl @@ -102,3 +102,29 @@ body[data-theme="solarized-dark"] .control-confirmButton colorSolarizedDarkPrimaryButton() + +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() diff --git a/browser/main/modals/NewNoteModal.styl b/browser/main/modals/NewNoteModal.styl index 748ab88c..db14133f 100644 --- a/browser/main/modals/NewNoteModal.styl +++ b/browser/main/modals/NewNoteModal.styl @@ -81,3 +81,19 @@ body[data-theme="solarized-dark"] .description color $ui-solarized-dark-text-color +body[data-theme="monokai"] + .root + background-color transparent + + .header + color $ui-monokai-text-color + + .control-button + border-color $ui-monokai-borderColor + color $ui-monokai-text-color + background-color transparent + &:focus + colorDarkPrimaryButton() + + .description + color $ui-monokai-text-color diff --git a/browser/main/modals/PreferencesModal/ConfigTab.styl b/browser/main/modals/PreferencesModal/ConfigTab.styl index f6f7ace9..0e5f81fb 100644 --- a/browser/main/modals/PreferencesModal/ConfigTab.styl +++ b/browser/main/modals/PreferencesModal/ConfigTab.styl @@ -133,6 +133,11 @@ colorSolarizedDarkControl() background-color $ui-solarized-dark-button-backgroundColor color $ui-solarized-dark-text-color +colorMonokaiControl() + border none + background-color $ui-monokai-button-backgroundColor + color $ui-monokai-text-color + body[data-theme="dark"] .root @@ -189,4 +194,29 @@ body[data-theme="solarized-dark"] select, .group-section-control-input colorSolarizedDarkControl() +body[data-theme="monokai"] + .root + color $ui-monokai-text-color + .group-header + color $ui-monokai-text-color + border-color $ui-monokai-borderColor + + .group-header2 + color $ui-monokai-text-color + + .group-section-control-input + border-color $ui-monokai-borderColor + + .group-control + border-color $ui-monokai-borderColor + .group-control-leftButton + colorDarkDefaultButton() + border-color $ui-monokai-borderColor + .group-control-rightButton + colorMonokaiPrimaryButton() + .group-hint + colorMonokaiControl() + .group-section-control + select, .group-section-control-input + colorMonokaiControl() diff --git a/browser/main/modals/PreferencesModal/Crowdfunding.styl b/browser/main/modals/PreferencesModal/Crowdfunding.styl index 930c33f0..3d4af539 100644 --- a/browser/main/modals/PreferencesModal/Crowdfunding.styl +++ b/browser/main/modals/PreferencesModal/Crowdfunding.styl @@ -33,4 +33,10 @@ body[data-theme="solarized-dark"] .root color $ui-solarized-dark-text-color p - color $ui-solarized-dark-text-color \ No newline at end of file + color $ui-solarized-dark-text-color + +body[data-theme="monokai"] + .root + color $ui-monokai-text-color + p + color $ui-monokai-text-color diff --git a/browser/main/modals/PreferencesModal/FolderItem.styl b/browser/main/modals/PreferencesModal/FolderItem.styl index acc4cbfb..8bcf2b02 100644 --- a/browser/main/modals/PreferencesModal/FolderItem.styl +++ b/browser/main/modals/PreferencesModal/FolderItem.styl @@ -126,3 +126,26 @@ body[data-theme="solarized-dark"] .folderItem-right-dangerButton colorSolarizedDarkPrimaryButton() + +body[data-theme="monokai"] + .folderItem + &:hover + background-color $ui-monokai-button-backgroundColor + + .folderItem-left-danger + color $danger-color + + .folderItem-left-key + color $ui-dark-inactive-text-color + + .folderItem-left-colorButton + colorMonokaiPrimaryButton() + + .folderItem-right-button + colorMonokaiPrimaryButton() + + .folderItem-right-confirmButton + colorMonokaiPrimaryButton() + + .folderItem-right-dangerButton + colorMonokaiPrimaryButton() diff --git a/browser/main/modals/PreferencesModal/HotkeyTab.js b/browser/main/modals/PreferencesModal/HotkeyTab.js index 8cbf772f..671e1516 100644 --- a/browser/main/modals/PreferencesModal/HotkeyTab.js +++ b/browser/main/modals/PreferencesModal/HotkeyTab.js @@ -67,7 +67,8 @@ class HotkeyTab extends React.Component { handleHotkeyChange (e) { const { config } = this.state config.hotkey = { - toggleMain: this.refs.toggleMain.value + toggleMain: this.refs.toggleMain.value, + toggleMode: this.refs.toggleMode.value } this.setState({ config @@ -115,6 +116,17 @@ class HotkeyTab extends React.Component { />
+
+
{i18n.__('Toggle editor mode')}
+
+ this.handleHotkeyChange(e)} + ref='toggleMode' + value={config.hotkey.toggleMode} + type='text' + /> +
+
@@ -474,6 +477,16 @@ class UiTab extends React.Component { Enable smart quotes +
+ +
@@ -543,6 +556,19 @@ class UiTab extends React.Component { />
+
+
+ {i18n.__('PlantUML Server')} +
+
+ this.handleUIChange(e)} + type='text' + /> +
+
diff --git a/tests/dataApi/attachmentManagement.test.js b/tests/dataApi/attachmentManagement.test.js index c90e1961..b61d8bf9 100644 --- a/tests/dataApi/attachmentManagement.test.js +++ b/tests/dataApi/attachmentManagement.test.js @@ -7,6 +7,9 @@ const findStorage = require('browser/lib/findStorage') jest.mock('unique-slug') const uniqueSlug = require('unique-slug') const mdurl = require('mdurl') +const fse = require('fs-extra') +jest.mock('sander') +const sander = require('sander') const systemUnderTest = require('browser/main/lib/dataApi/attachmentManagement') @@ -48,11 +51,13 @@ it('should test that copyAttachment works correctly assuming correct working of const noteKey = 'noteKey' const dummyUniquePath = 'dummyPath' const dummyStorage = {path: 'dummyStoragePath'} + const dummyReadStream = {} + dummyReadStream.pipe = jest.fn() + dummyReadStream.on = jest.fn((event, callback) => { callback() }) fs.existsSync = jest.fn() fs.existsSync.mockReturnValue(true) - fs.createReadStream = jest.fn() - fs.createReadStream.mockReturnValue({pipe: jest.fn()}) + fs.createReadStream = jest.fn(() => dummyReadStream) fs.createWriteStream = jest.fn() findStorage.findStorage = jest.fn() @@ -75,7 +80,11 @@ it('should test that copyAttachment creates a new folder if the attachment folde const noteKey = 'noteKey' const attachmentFolderPath = path.join(dummyStorage.path, systemUnderTest.DESTINATION_FOLDER) const attachmentFolderNoteKyPath = path.join(dummyStorage.path, systemUnderTest.DESTINATION_FOLDER, noteKey) + const dummyReadStream = {} + dummyReadStream.pipe = jest.fn() + dummyReadStream.on = jest.fn() + fs.createReadStream = jest.fn(() => dummyReadStream) fs.existsSync = jest.fn() fs.existsSync.mockReturnValueOnce(true) fs.existsSync.mockReturnValueOnce(false) @@ -97,7 +106,11 @@ it('should test that copyAttachment creates a new folder if the attachment folde it('should test that copyAttachment don\'t uses a random file name if not intended ', function () { const dummyStorage = {path: 'dummyStoragePath'} + const dummyReadStream = {} + dummyReadStream.pipe = jest.fn() + dummyReadStream.on = jest.fn() + fs.createReadStream = jest.fn(() => dummyReadStream) fs.existsSync = jest.fn() fs.existsSync.mockReturnValueOnce(true) fs.existsSync.mockReturnValueOnce(false) @@ -142,13 +155,13 @@ it('should replace the all ":storage" path with the actual storage path', functi ' \n' + '

Headline

\n' + '

\n' + - ' dummyImage.png\n' + + ' dummyImage.png\n' + '

\n' + '

\n' + - ' dummyPDF.pdf\n' + + ' dummyPDF.pdf\n' + '

\n' + '

\n' + - ' dummyImage2.jpg\n' + + ' dummyImage2.jpg\n' + '

\n' + ' \n' + '' @@ -166,3 +179,291 @@ it('should test that generateAttachmentMarkdown works correct both with previews actual = systemUnderTest.generateAttachmentMarkdown(fileName, path, false) expect(actual).toEqual(expected) }) + +it('should test that getAttachmentsInContent finds all attachments', function () { + const testInput = + '\n' + + ' \n' + + ' //header\n' + + ' \n' + + ' \n' + + '

Headline

\n' + + '

\n' + + ' dummyImage.png\n' + + '

\n' + + '

\n' + + ' dummyPDF.pdf\n' + + '

\n' + + '

\n' + + ' dummyImage2.jpg\n' + + '

\n' + + ' \n' + + '' + const actual = systemUnderTest.getAttachmentsInContent(testInput) + const expected = [':storage' + path.sep + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + path.sep + '0.6r4zdgc22xp', ':storage' + path.sep + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + path.sep + '0.q2i4iw0fyx', ':storage' + path.sep + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + path.sep + 'd6c5ee92.jpg'] + expect(actual).toEqual(expect.arrayContaining(expected)) +}) + +it('should test that getAbsolutePathsOfAttachmentsInContent returns all absolute paths', function () { + const dummyStoragePath = 'dummyStoragePath' + const testInput = + '\n' + + ' \n' + + ' //header\n' + + ' \n' + + ' \n' + + '

Headline

\n' + + '

\n' + + ' dummyImage.png\n' + + '

\n' + + '

\n' + + ' dummyPDF.pdf\n' + + '

\n' + + '

\n' + + ' dummyImage2.jpg\n' + + '

\n' + + ' \n' + + '' + const actual = systemUnderTest.getAbsolutePathsOfAttachmentsInContent(testInput, dummyStoragePath) + const expected = [dummyStoragePath + path.sep + systemUnderTest.DESTINATION_FOLDER + path.sep + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + path.sep + '0.6r4zdgc22xp', + dummyStoragePath + path.sep + systemUnderTest.DESTINATION_FOLDER + path.sep + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + path.sep + '0.q2i4iw0fyx', + dummyStoragePath + path.sep + systemUnderTest.DESTINATION_FOLDER + path.sep + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + path.sep + 'd6c5ee92.jpg'] + expect(actual).toEqual(expect.arrayContaining(expected)) +}) + +it('should remove the all ":storage" and noteKey references', function () { + const storageFolder = systemUnderTest.DESTINATION_FOLDER + const noteKey = 'noteKey' + const testInput = + '\n' + + ' \n' + + ' //header\n' + + ' \n' + + ' \n' + + '

Headline

\n' + + '

\n' + + ' dummyImage.png\n' + + '

\n' + + '

\n' + + ' dummyPDF.pdf\n' + + '

\n' + + '

\n' + + ' dummyImage2.jpg\n' + + '

\n' + + ' \n' + + '' + const expectedOutput = + '\n' + + ' \n' + + ' //header\n' + + ' \n' + + ' \n' + + '

Headline

\n' + + '

\n' + + ' dummyImage.png\n' + + '

\n' + + '

\n' + + ' dummyPDF.pdf\n' + + '

\n' + + '

\n' + + ' dummyImage2.jpg\n' + + '

\n' + + ' \n' + + '' + const actual = systemUnderTest.removeStorageAndNoteReferences(testInput, noteKey) + expect(actual).toEqual(expectedOutput) +}) + +it('should delete the correct attachment folder if a note is deleted', function () { + const dummyStorage = {path: 'dummyStoragePath'} + const storageKey = 'storageKey' + const noteKey = 'noteKey' + findStorage.findStorage = jest.fn(() => dummyStorage) + sander.rimrafSync = jest.fn() + + const expectedPathToBeDeleted = path.join(dummyStorage.path, systemUnderTest.DESTINATION_FOLDER, noteKey) + systemUnderTest.deleteAttachmentFolder(storageKey, noteKey) + expect(findStorage.findStorage).toHaveBeenCalledWith(storageKey) + expect(sander.rimrafSync).toHaveBeenCalledWith(expectedPathToBeDeleted) +}) + +it('should test that deleteAttachmentsNotPresentInNote deletes all unreferenced attachments ', function () { + const dummyStorage = {path: 'dummyStoragePath'} + const noteKey = 'noteKey' + const storageKey = 'storageKey' + const markdownContent = '' + const dummyFilesInFolder = ['file1.txt', 'file2.pdf', 'file3.jpg'] + const attachmentFolderPath = path.join(dummyStorage.path, systemUnderTest.DESTINATION_FOLDER, noteKey) + + findStorage.findStorage = jest.fn(() => dummyStorage) + fs.existsSync = jest.fn(() => true) + fs.readdir = jest.fn((paht, callback) => callback(undefined, dummyFilesInFolder)) + fs.unlink = jest.fn() + + systemUnderTest.deleteAttachmentsNotPresentInNote(markdownContent, storageKey, noteKey) + expect(fs.existsSync).toHaveBeenLastCalledWith(attachmentFolderPath) + expect(fs.readdir).toHaveBeenCalledTimes(1) + expect(fs.readdir.mock.calls[0][0]).toBe(attachmentFolderPath) + + expect(fs.unlink).toHaveBeenCalledTimes(dummyFilesInFolder.length) + const fsUnlinkCallArguments = [] + for (let i = 0; i < dummyFilesInFolder.length; i++) { + fsUnlinkCallArguments.push(fs.unlink.mock.calls[i][0]) + } + + dummyFilesInFolder.forEach(function (file) { + expect(fsUnlinkCallArguments.includes(path.join(attachmentFolderPath, file))).toBe(true) + }) +}) + +it('should test that deleteAttachmentsNotPresentInNote does not delete referenced attachments', function () { + const dummyStorage = {path: 'dummyStoragePath'} + const noteKey = 'noteKey' + const storageKey = 'storageKey' + const dummyFilesInFolder = ['file1.txt', 'file2.pdf', 'file3.jpg'] + const markdownContent = systemUnderTest.generateAttachmentMarkdown('fileLabel', path.join(systemUnderTest.STORAGE_FOLDER_PLACEHOLDER, noteKey, dummyFilesInFolder[0]), false) + const attachmentFolderPath = path.join(dummyStorage.path, systemUnderTest.DESTINATION_FOLDER, noteKey) + + findStorage.findStorage = jest.fn(() => dummyStorage) + fs.existsSync = jest.fn(() => true) + fs.readdir = jest.fn((paht, callback) => callback(undefined, dummyFilesInFolder)) + fs.unlink = jest.fn() + + systemUnderTest.deleteAttachmentsNotPresentInNote(markdownContent, storageKey, noteKey) + + expect(fs.unlink).toHaveBeenCalledTimes(dummyFilesInFolder.length - 1) + const fsUnlinkCallArguments = [] + for (let i = 0; i < dummyFilesInFolder.length - 1; i++) { + fsUnlinkCallArguments.push(fs.unlink.mock.calls[i][0]) + } + expect(fsUnlinkCallArguments.includes(path.join(attachmentFolderPath, dummyFilesInFolder[0]))).toBe(false) +}) + +it('should test that moveAttachments moves attachments only if the source folder existed', function () { + fse.existsSync = jest.fn(() => false) + fse.moveSync = jest.fn() + + const oldPath = 'oldPath' + const newPath = 'newPath' + const oldNoteKey = 'oldNoteKey' + const newNoteKey = 'newNoteKey' + const content = '' + + const expectedSource = path.join(oldPath, systemUnderTest.DESTINATION_FOLDER, oldNoteKey) + + systemUnderTest.moveAttachments(oldPath, newPath, oldNoteKey, newNoteKey, content) + expect(fse.existsSync).toHaveBeenCalledWith(expectedSource) + expect(fse.moveSync).not.toHaveBeenCalled() +}) + +it('should test that moveAttachments moves attachments to the right destination', function () { + fse.existsSync = jest.fn(() => true) + fse.moveSync = jest.fn() + + const oldPath = 'oldPath' + const newPath = 'newPath' + const oldNoteKey = 'oldNoteKey' + const newNoteKey = 'newNoteKey' + const content = '' + + const expectedSource = path.join(oldPath, systemUnderTest.DESTINATION_FOLDER, oldNoteKey) + const expectedDestination = path.join(newPath, systemUnderTest.DESTINATION_FOLDER, newNoteKey) + + systemUnderTest.moveAttachments(oldPath, newPath, oldNoteKey, newNoteKey, content) + expect(fse.existsSync).toHaveBeenCalledWith(expectedSource) + expect(fse.moveSync).toHaveBeenCalledWith(expectedSource, expectedDestination) +}) + +it('should test that moveAttachments returns a correct modified content version', function () { + fse.existsSync = jest.fn() + fse.moveSync = jest.fn() + + const oldPath = 'oldPath' + const newPath = 'newPath' + const oldNoteKey = 'oldNoteKey' + const newNoteKey = 'newNoteKey' + const testInput = + 'Test input' + + '![' + systemUnderTest.STORAGE_FOLDER_PLACEHOLDER + path.sep + oldNoteKey + path.sep + 'image.jpg](imageName}) \n' + + '[' + systemUnderTest.STORAGE_FOLDER_PLACEHOLDER + path.sep + oldNoteKey + path.sep + 'pdf.pdf](pdf})' + const expectedOutput = + 'Test input' + + '![' + systemUnderTest.STORAGE_FOLDER_PLACEHOLDER + path.sep + newNoteKey + path.sep + 'image.jpg](imageName}) \n' + + '[' + systemUnderTest.STORAGE_FOLDER_PLACEHOLDER + path.sep + newNoteKey + path.sep + 'pdf.pdf](pdf})' + + const actualContent = systemUnderTest.moveAttachments(oldPath, newPath, oldNoteKey, newNoteKey, testInput) + expect(actualContent).toBe(expectedOutput) +}) + +it('should test that cloneAttachments modifies the content of the new note correctly', function () { + const oldNote = {key: 'oldNoteKey', content: 'oldNoteContent', storage: 'storageKey', type: 'MARKDOWN_NOTE'} + const newNote = {key: 'newNoteKey', content: 'oldNoteContent', storage: 'storageKey', type: 'MARKDOWN_NOTE'} + const testInput = + 'Test input' + + '![' + systemUnderTest.STORAGE_FOLDER_PLACEHOLDER + path.sep + oldNote.key + path.sep + 'image.jpg](imageName}) \n' + + '[' + systemUnderTest.STORAGE_FOLDER_PLACEHOLDER + path.sep + oldNote.key + path.sep + 'pdf.pdf](pdf})' + newNote.content = testInput + + const expectedOutput = + 'Test input' + + '![' + systemUnderTest.STORAGE_FOLDER_PLACEHOLDER + path.sep + newNote.key + path.sep + 'image.jpg](imageName}) \n' + + '[' + systemUnderTest.STORAGE_FOLDER_PLACEHOLDER + path.sep + newNote.key + path.sep + 'pdf.pdf](pdf})' + systemUnderTest.cloneAttachments(oldNote, newNote) + + expect(newNote.content).toBe(expectedOutput) +}) + +it('should test that cloneAttachments finds all attachments and copies them to the new location', function () { + const storagePathOld = 'storagePathOld' + const storagePathNew = 'storagePathNew' + const dummyStorageOld = {path: storagePathOld} + const dummyStorageNew = {path: storagePathNew} + const oldNote = {key: 'oldNoteKey', content: 'oldNoteContent', storage: 'storageKeyOldNote', type: 'MARKDOWN_NOTE'} + const newNote = {key: 'newNoteKey', content: 'oldNoteContent', storage: 'storageKeyNewNote', type: 'MARKDOWN_NOTE'} + const testInput = + 'Test input' + + '![' + systemUnderTest.STORAGE_FOLDER_PLACEHOLDER + path.sep + oldNote.key + path.sep + 'image.jpg](imageName}) \n' + + '[' + systemUnderTest.STORAGE_FOLDER_PLACEHOLDER + path.sep + oldNote.key + path.sep + 'pdf.pdf](pdf})' + oldNote.content = testInput + newNote.content = testInput + + const copyFileSyncResp = {to: jest.fn()} + sander.copyFileSync = jest.fn() + sander.copyFileSync.mockReturnValue(copyFileSyncResp) + findStorage.findStorage = jest.fn() + findStorage.findStorage.mockReturnValueOnce(dummyStorageOld) + findStorage.findStorage.mockReturnValue(dummyStorageNew) + + const pathAttachmentOneFrom = path.join(storagePathOld, systemUnderTest.DESTINATION_FOLDER, oldNote.key, 'image.jpg') + const pathAttachmentOneTo = path.join(storagePathNew, systemUnderTest.DESTINATION_FOLDER, newNote.key, 'image.jpg') + + const pathAttachmentTwoFrom = path.join(storagePathOld, systemUnderTest.DESTINATION_FOLDER, oldNote.key, 'pdf.pdf') + const pathAttachmentTwoTo = path.join(storagePathNew, systemUnderTest.DESTINATION_FOLDER, newNote.key, 'pdf.pdf') + + systemUnderTest.cloneAttachments(oldNote, newNote) + + expect(findStorage.findStorage).toHaveBeenCalledWith(oldNote.storage) + expect(findStorage.findStorage).toHaveBeenCalledWith(newNote.storage) + expect(sander.copyFileSync).toHaveBeenCalledTimes(2) + expect(copyFileSyncResp.to).toHaveBeenCalledTimes(2) + expect(sander.copyFileSync.mock.calls[0][0]).toBe(pathAttachmentOneFrom) + expect(copyFileSyncResp.to.mock.calls[0][0]).toBe(pathAttachmentOneTo) + expect(sander.copyFileSync.mock.calls[1][0]).toBe(pathAttachmentTwoFrom) + expect(copyFileSyncResp.to.mock.calls[1][0]).toBe(pathAttachmentTwoTo) +}) + +it('should test that cloneAttachments finds all attachments and copies them to the new location', function () { + const oldNote = {key: 'oldNoteKey', content: 'oldNoteContent', storage: 'storageKeyOldNote', type: 'SOMETHING_ELSE'} + const newNote = {key: 'newNoteKey', content: 'oldNoteContent', storage: 'storageKeyNewNote', type: 'SOMETHING_ELSE'} + const testInput = 'Test input' + oldNote.content = testInput + newNote.content = testInput + + sander.copyFileSync = jest.fn() + findStorage.findStorage = jest.fn() + + systemUnderTest.cloneAttachments(oldNote, newNote) + + expect(findStorage.findStorage).not.toHaveBeenCalled() + expect(sander.copyFileSync).not.toHaveBeenCalled() +}) diff --git a/tests/dataApi/deleteFolder-test.js b/tests/dataApi/deleteFolder-test.js index 1d9c7646..af901896 100644 --- a/tests/dataApi/deleteFolder-test.js +++ b/tests/dataApi/deleteFolder-test.js @@ -1,5 +1,9 @@ const test = require('ava') const deleteFolder = require('browser/main/lib/dataApi/deleteFolder') +const attachmentManagement = require('browser/main/lib/dataApi/attachmentManagement') +const createNote = require('browser/main/lib/dataApi/createNote') +const fs = require('fs') +const faker = require('faker') global.document = require('jsdom').jsdom('') global.window = document.defaultView @@ -24,8 +28,32 @@ test.beforeEach((t) => { test.serial('Delete a folder', (t) => { const storageKey = t.context.storage.cache.key const folderKey = t.context.storage.json.folders[0].key + let noteKey + + const input1 = { + type: 'SNIPPET_NOTE', + description: faker.lorem.lines(), + snippets: [{ + name: faker.system.fileName(), + mode: 'text', + content: faker.lorem.lines() + }], + tags: faker.lorem.words().split(' '), + folder: folderKey + } + input1.title = input1.description.split('\n').shift() return Promise.resolve() + .then(function prepare () { + return createNote(storageKey, input1) + .then(function createAttachmentFolder (data) { + fs.mkdirSync(path.join(storagePath, attachmentManagement.DESTINATION_FOLDER)) + fs.mkdirSync(path.join(storagePath, attachmentManagement.DESTINATION_FOLDER, data.key)) + noteKey = data.key + + return data + }) + }) .then(function doTest () { return deleteFolder(storageKey, folderKey) }) @@ -36,6 +64,9 @@ test.serial('Delete a folder', (t) => { t.true(_.find(jsonData.folders, {key: folderKey}) == null) const notePaths = sander.readdirSync(data.storage.path, 'notes') t.is(notePaths.length, t.context.storage.notes.filter((note) => note.folder !== folderKey).length) + + const attachmentFolderPath = path.join(storagePath, attachmentManagement.DESTINATION_FOLDER, noteKey) + t.false(fs.existsSync(attachmentFolderPath)) }) }) diff --git a/tests/dataApi/deleteNote-test.js b/tests/dataApi/deleteNote-test.js index 611022de..9c809dcf 100644 --- a/tests/dataApi/deleteNote-test.js +++ b/tests/dataApi/deleteNote-test.js @@ -14,6 +14,8 @@ const sander = require('sander') const os = require('os') const CSON = require('@rokt33r/season') const faker = require('faker') +const fs = require('fs') +const attachmentManagement = require('browser/main/lib/dataApi/attachmentManagement') const storagePath = path.join(os.tmpdir(), 'test/delete-note') @@ -42,6 +44,11 @@ test.serial('Delete a note', (t) => { return Promise.resolve() .then(function doTest () { return createNote(storageKey, input1) + .then(function createAttachmentFolder (data) { + fs.mkdirSync(path.join(storagePath, attachmentManagement.DESTINATION_FOLDER)) + fs.mkdirSync(path.join(storagePath, attachmentManagement.DESTINATION_FOLDER, data.key)) + return data + }) .then(function (data) { return deleteNote(storageKey, data.key) }) @@ -52,8 +59,13 @@ test.serial('Delete a note', (t) => { t.fail('note cson must be deleted.') } catch (err) { t.is(err.code, 'ENOENT') + return data } }) + .then(function assertAttachmentFolderDeleted (data) { + const attachmentFolderPath = path.join(storagePath, attachmentManagement.DESTINATION_FOLDER, data.noteKey) + t.is(fs.existsSync(attachmentFolderPath), false) + }) }) test.after(function after () { diff --git a/tests/dataApi/exportFolder-test.js b/tests/dataApi/exportFolder-test.js index ee6fb898..fb4aaa7b 100644 --- a/tests/dataApi/exportFolder-test.js +++ b/tests/dataApi/exportFolder-test.js @@ -13,6 +13,7 @@ const TestDummy = require('../fixtures/TestDummy') const os = require('os') const faker = require('faker') const fs = require('fs') +const sander = require('sander') const storagePath = path.join(os.tmpdir(), 'test/export-note') @@ -60,3 +61,8 @@ test.serial('Export a folder', (t) => { t.false(fs.existsSync(filePath)) }) }) + +test.after.always(function after () { + localStorage.clear() + sander.rimrafSync(storagePath) +}) diff --git a/tests/fixtures/markdowns.js b/tests/fixtures/markdowns.js index 8db35485..69e335e0 100644 --- a/tests/fixtures/markdowns.js +++ b/tests/fixtures/markdowns.js @@ -48,10 +48,13 @@ const checkboxes = ` const smartQuotes = 'This is a "QUOTE".' +const breaks = 'This is the first line.\nThis is the second line.' + export default { basic, codeblock, katex, checkboxes, - smartQuotes + smartQuotes, + breaks } diff --git a/tests/lib/markdown-test.js b/tests/lib/markdown-test.js index b2a81fdf..73b68799 100644 --- a/tests/lib/markdown-test.js +++ b/tests/lib/markdown-test.js @@ -34,3 +34,12 @@ test('Markdown.render() should text with quotes correctly', t => { const renderedNonSmartQuotes = newmd.render(markdownFixtures.smartQuotes) t.snapshot(renderedNonSmartQuotes) }) + +test('Markdown.render() should render line breaks correctly', t => { + const renderedBreaks = md.render(markdownFixtures.breaks) + t.snapshot(renderedBreaks) + + const newmd = new Markdown({ breaks: false }) + const renderedNonBreaks = newmd.render(markdownFixtures.breaks) + t.snapshot(renderedNonBreaks) +}) diff --git a/tests/lib/snapshots/markdown-test.js.md b/tests/lib/snapshots/markdown-test.js.md index d4f0469e..ffc3d699 100644 --- a/tests/lib/snapshots/markdown-test.js.md +++ b/tests/lib/snapshots/markdown-test.js.md @@ -4,6 +4,20 @@ The actual snapshot is saved in `markdown-test.js.snap`. Generated by [AVA](https://ava.li). +## Markdown.render() should render line breaks correctly + +> Snapshot 1 + + `

This is the first line.
␊ + This is the second line.

␊ + ` + +> Snapshot 2 + + `

This is the first line.␊ + This is the second line.

␊ + ` + ## Markdown.render() should renders KaTeX correctly > Snapshot 1 diff --git a/tests/lib/snapshots/markdown-test.js.snap b/tests/lib/snapshots/markdown-test.js.snap index 71ff221d..fc310cfd 100644 Binary files a/tests/lib/snapshots/markdown-test.js.snap and b/tests/lib/snapshots/markdown-test.js.snap differ diff --git a/yarn.lock b/yarn.lock index a8817ebd..73317245 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1864,7 +1864,7 @@ codemirror-mode-elixir@^1.1.1: dependencies: codemirror "^5.20.2" -codemirror@^5.18.2, codemirror@^5.19.0: +codemirror@^5.18.2: version "5.26.0" resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.26.0.tgz#bcbee86816ed123870c260461c2b5c40b68746e5" @@ -1872,6 +1872,10 @@ codemirror@^5.20.2: version "5.33.0" resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.33.0.tgz#462ad9a6fe8d38b541a9536a3997e1ef93b40c6a" +codemirror@^5.37.0: + version "5.37.0" + resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.37.0.tgz#c349b584e158f590277f26d37c2469a6bc538036" + coffee-script@^1.10.0: version "1.12.6" resolved "https://registry.yarnpkg.com/coffee-script/-/coffee-script-1.12.6.tgz#285a3f7115689065064d6bf9ef4572db66695cbf" @@ -3644,6 +3648,14 @@ fs-extra@^1.0.0: jsonfile "^2.1.0" klaw "^1.0.0" +fs-extra@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-5.0.0.tgz#414d0110cdd06705734d055652c5411260c31abd" + dependencies: + graceful-fs "^4.1.2" + jsonfile "^4.0.0" + universalify "^0.1.0" + fs-plus@2.x: version "2.10.1" resolved "https://registry.yarnpkg.com/fs-plus/-/fs-plus-2.10.1.tgz#3204781d7840611e6364e7b6fb058c96327c5aa5" @@ -5316,6 +5328,12 @@ jsonfile@^2.0.0, jsonfile@^2.1.0: optionalDependencies: graceful-fs "^4.1.6" +jsonfile@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" + optionalDependencies: + graceful-fs "^4.1.6" + jsonify@~0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" @@ -5967,6 +5985,14 @@ moment@^2.10.3: version "2.18.1" resolved "https://registry.yarnpkg.com/moment/-/moment-2.18.1.tgz#c36193dd3ce1c2eed2adb7c802dbbc77a81b1c0f" +mousetrap-global-bind@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/mousetrap-global-bind/-/mousetrap-global-bind-1.1.0.tgz#cd7de9222bd0646fa2e010d54c84a74c26a88edd" + +mousetrap@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/mousetrap/-/mousetrap-1.6.1.tgz#2a085f5c751294c75e7e81f6ec2545b29cbf42d9" + ms@0.7.1: version "0.7.1" resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.1.tgz#9cd13c03adbff25b65effde7ce864ee952017098" @@ -8749,6 +8775,10 @@ unique-temp-dir@^1.0.0: os-tmpdir "^1.0.1" uid2 "0.0.3" +universalify@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.1.tgz#fa71badd4437af4c148841e3b3b165f9e9e590b7" + unpipe@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"