diff --git a/browser/components/CodeEditor.js b/browser/components/CodeEditor.js index 0ddfd5c9..2f2b1551 100644 --- a/browser/components/CodeEditor.js +++ b/browser/components/CodeEditor.js @@ -14,6 +14,8 @@ import { import TextEditorInterface from 'browser/lib/TextEditorInterface' import eventEmitter from 'browser/main/lib/eventEmitter' import iconv from 'iconv-lite' + +import { isMarkdownTitleURL } from 'browser/lib/utils' import styles from '../components/CodeEditor.styl' const { ipcRenderer, remote, clipboard } = require('electron') import normalizeEditorFontFamily from 'browser/lib/normalizeEditorFontFamily' @@ -22,6 +24,8 @@ const buildEditorContextMenu = require('browser/lib/contextMenuBuilder') import TurndownService from 'turndown' import {languageMaps} from '../lib/CMLanguageList' import snippetManager from '../lib/SnippetManager' +import {generateInEditor, tocExistsInEditor} from 'browser/lib/markdown-toc-generator' +import markdownlint from 'markdownlint' CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js' @@ -34,6 +38,38 @@ function translateHotkey (hotkey) { return hotkey.replace(/\s*\+\s*/g, '-').replace(/Command/g, 'Cmd').replace(/Control/g, 'Ctrl') } +const validatorOfMarkdown = (text, updateLinting) => { + const lintOptions = { + 'strings': { + 'content': text + } + } + + return markdownlint(lintOptions, (err, result) => { + if (!err) { + const foundIssues = [] + result.content.map(item => { + let ruleNames = '' + item.ruleNames.map((ruleName, index) => { + ruleNames += ruleName + if (index === item.ruleNames.length - 1) { + ruleNames += ': ' + } else { + ruleNames += '/' + } + }) + foundIssues.push({ + from: CodeMirror.Pos(item.lineNumber, 0), + to: CodeMirror.Pos(item.lineNumber, 1), + message: ruleNames + item.ruleDescription, + severity: 'warning' + }) + }) + updateLinting(foundIssues) + } + }) +} + export default class CodeEditor extends React.Component { constructor (props) { super(props) @@ -197,6 +233,26 @@ export default class CodeEditor extends React.Component { 'Cmd-T': function (cm) { // Do nothing }, + 'Ctrl-/': function (cm) { + if (global.process.platform === 'darwin') { return } + const dateNow = new Date() + cm.replaceSelection(dateNow.toLocaleDateString()) + }, + 'Cmd-/': function (cm) { + if (global.process.platform !== 'darwin') { return } + const dateNow = new Date() + cm.replaceSelection(dateNow.toLocaleDateString()) + }, + 'Shift-Ctrl-/': function (cm) { + if (global.process.platform === 'darwin') { return } + const dateNow = new Date() + cm.replaceSelection(dateNow.toLocaleString()) + }, + 'Shift-Cmd-/': function (cm) { + if (global.process.platform !== 'darwin') { return } + const dateNow = new Date() + cm.replaceSelection(dateNow.toLocaleString()) + }, Enter: 'boostNewLineAndIndentContinueMarkdownList', 'Ctrl-C': cm => { if (cm.getOption('keyMap').substr(0, 3) === 'vim') { @@ -233,6 +289,7 @@ export default class CodeEditor extends React.Component { snippetManager.init() this.updateDefaultKeyMap() + const checkMarkdownNoteIsOpening = this.props.mode === 'Boost Flavored Markdown' this.value = this.props.value this.editor = CodeMirror(this.refs.root, { rulers: buildCMRulers(rulers, enableRulers), @@ -249,7 +306,11 @@ export default class CodeEditor extends React.Component { inputStyle: 'textarea', dragDrop: false, foldGutter: true, - gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'], + lint: checkMarkdownNoteIsOpening ? { + 'getAnnotations': validatorOfMarkdown, + 'async': true + } : false, + gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter', 'CodeMirror-lint-markers'], autoCloseBrackets: { pairs: this.props.matchingPairs, triples: this.props.matchingTriples, @@ -594,6 +655,34 @@ export default class CodeEditor extends React.Component { handleChange (editor, changeObject) { spellcheck.handleChange(editor, changeObject) + // The current note contains an toc. We'll check for changes on headlines. + // origin is undefined when markdownTocGenerator replace the old tod + if (tocExistsInEditor(editor) && changeObject.origin !== undefined) { + let requireTocUpdate + + // Check if one of the changed lines contains a headline + for (let line = 0; line < changeObject.text.length; line++) { + if (this.linePossibleContainsHeadline(editor.getLine(changeObject.from.line + line))) { + requireTocUpdate = true + break + } + } + + if (!requireTocUpdate) { + // Check if one of the removed lines contains a headline + for (let line = 0; line < changeObject.removed.length; line++) { + if (this.linePossibleContainsHeadline(changeObject.removed[line])) { + requireTocUpdate = true + break + } + } + } + + if (requireTocUpdate) { + generateInEditor(editor) + } + } + this.updateHighlight(editor, changeObject) this.value = editor.getValue() @@ -602,6 +691,12 @@ export default class CodeEditor extends React.Component { } } + linePossibleContainsHeadline (currentLine) { + // We can't check if the line start with # because when some write text before + // the # we also need to update the toc + return currentLine.includes('# ') + } + incrementLines (start, linesAdded, linesRemoved, editor) { const highlightedLines = editor.options.linesHighlighted @@ -809,6 +904,8 @@ export default class CodeEditor extends React.Component { if (isInFencedCodeBlock(editor)) { this.handlePasteText(editor, pastedTxt) + } else if (fetchUrlTitle && isMarkdownTitleURL(pastedTxt) && !isInLinkTag(editor)) { + this.handlePasteUrl(editor, pastedTxt) } else if (fetchUrlTitle && isURL(pastedTxt) && !isInLinkTag(editor)) { this.handlePasteUrl(editor, pastedTxt) } else if (attachmentManagement.isAttachmentLink(pastedTxt)) { @@ -850,7 +947,17 @@ export default class CodeEditor extends React.Component { } handlePasteUrl (editor, pastedTxt) { - const taggedUrl = `<${pastedTxt}>` + let taggedUrl = `<${pastedTxt}>` + let urlToFetch = pastedTxt + let titleMark = '' + + if (isMarkdownTitleURL(pastedTxt)) { + const pastedTxtSplitted = pastedTxt.split(' ') + titleMark = `${pastedTxtSplitted[0]} ` + urlToFetch = pastedTxtSplitted[1] + taggedUrl = `<${urlToFetch}>` + } + editor.replaceSelection(taggedUrl) const isImageReponse = response => { @@ -862,22 +969,23 @@ export default class CodeEditor extends React.Component { const replaceTaggedUrl = replacement => { const value = editor.getValue() const cursor = editor.getCursor() - const newValue = value.replace(taggedUrl, replacement) + const newValue = value.replace(taggedUrl, titleMark + replacement) const newCursor = Object.assign({}, cursor, { - ch: cursor.ch + newValue.length - value.length + ch: cursor.ch + newValue.length - (value.length - titleMark.length) }) + editor.setValue(newValue) editor.setCursor(newCursor) } - fetch(pastedTxt, { + fetch(urlToFetch, { method: 'get' }) .then(response => { if (isImageReponse(response)) { - return this.mapImageResponse(response, pastedTxt) + return this.mapImageResponse(response, urlToFetch) } else { - return this.mapNormalResponse(response, pastedTxt) + return this.mapNormalResponse(response, urlToFetch) } }) .then(replacement => { diff --git a/browser/components/CodeEditor.styl b/browser/components/CodeEditor.styl index 7a254935..1aa0e466 100644 --- a/browser/components/CodeEditor.styl +++ b/browser/components/CodeEditor.styl @@ -3,4 +3,3 @@ .spellcheck-select border: none - text-decoration underline wavy red diff --git a/browser/components/MarkdownPreview.js b/browser/components/MarkdownPreview.js index 63d825f4..55b36243 100755 --- a/browser/components/MarkdownPreview.js +++ b/browser/components/MarkdownPreview.js @@ -8,7 +8,7 @@ import consts from 'browser/lib/consts' import Raphael from 'raphael' import flowchart from 'flowchart' import mermaidRender from './render/MermaidRender' -import SequenceDiagram from 'js-sequence-diagrams' +import SequenceDiagram from '@rokt33r/js-sequence-diagrams' import Chart from 'chart.js' import eventEmitter from 'browser/main/lib/eventEmitter' import htmlTextHelper from 'browser/lib/htmlTextHelper' @@ -255,7 +255,7 @@ export default class MarkdownPreview extends React.Component { return } // No contextMenu was passed to us -> execute our own link-opener - if (event.target.tagName.toLowerCase() === 'a') { + if (event.target.tagName.toLowerCase() === 'a' && event.target.getAttribute('href')) { const href = event.target.href const isLocalFile = href.startsWith('file:') if (isLocalFile) { @@ -282,33 +282,27 @@ export default class MarkdownPreview extends React.Component { handleMouseDown (e) { const config = ConfigManager.get() + const clickElement = e.target + const targetTag = clickElement.tagName // The direct parent HTML of where was clicked ie "BODY" or "DIV" + const lineNumber = getSourceLineNumberByElement(clickElement) // Line location of element clicked. + if (config.editor.switchPreview === 'RIGHTCLICK' && e.buttons === 2 && config.editor.type === 'SPLIT') { eventEmitter.emit('topbar:togglemodebutton', 'CODE') } if (e.ctrlKey) { if (config.editor.type === 'SPLIT') { - const clickElement = e.target - const lineNumber = getSourceLineNumberByElement(clickElement) if (lineNumber !== -1) { eventEmitter.emit('line:jump', lineNumber) } } else { - const clickElement = e.target - const lineNumber = getSourceLineNumberByElement(clickElement) if (lineNumber !== -1) { eventEmitter.emit('editor:focus') eventEmitter.emit('line:jump', lineNumber) } } } - if (e.target != null) { - switch (e.target.tagName) { - case 'A': - case 'INPUT': - return null - } - } - if (this.props.onMouseDown != null) this.props.onMouseDown(e) + + if (this.props.onMouseDown != null && targetTag === 'BODY') this.props.onMouseDown(e) } handleMouseUp (e) { @@ -676,14 +670,14 @@ export default class MarkdownPreview extends React.Component { ) } - GetCodeThemeLink (theme) { - theme = consts.THEMES.some(_theme => _theme === theme) && - theme !== 'default' - ? theme - : 'elegant' - return theme.startsWith('solarized') - ? `${appPath}/node_modules/codemirror/theme/solarized.css` - : `${appPath}/node_modules/codemirror/theme/${theme}.css` + GetCodeThemeLink (name) { + const theme = consts.THEMES.find(theme => theme.name === name) + + if (theme) { + return `${appPath}/${theme.path}` + } else { + return `${appPath}/node_modules/codemirror/theme/elegant.css` + } } rewriteIframe () { @@ -741,9 +735,9 @@ export default class MarkdownPreview extends React.Component { } ) - codeBlockTheme = consts.THEMES.some(_theme => _theme === codeBlockTheme) - ? codeBlockTheme - : 'default' + codeBlockTheme = consts.THEMES.find(theme => theme.name === codeBlockTheme) + + const codeBlockThemeClassName = codeBlockTheme ? codeBlockTheme.className : 'cm-s-default' _.forEach( this.refs.root.contentWindow.document.querySelectorAll('.code code'), @@ -766,14 +760,11 @@ export default class MarkdownPreview extends React.Component { }) } } + el.parentNode.appendChild(copyIcon) el.innerHTML = '' - if (codeBlockTheme.indexOf('solarized') === 0) { - const [refThema, color] = codeBlockTheme.split(' ') - el.parentNode.className += ` cm-s-${refThema} cm-s-${color}` - } else { - el.parentNode.className += ` cm-s-${codeBlockTheme}` - } + el.parentNode.className += ` ${codeBlockThemeClassName}` + CodeMirror.runMode(content, syntax.mime, el, { tabSize: indentSize }) @@ -888,78 +879,96 @@ export default class MarkdownPreview extends React.Component { const markdownPreviewIframe = document.querySelector('.MarkdownPreview') const rect = markdownPreviewIframe.getBoundingClientRect() + const config = { attributes: true, subtree: true } + const imgObserver = new MutationObserver((mutationList) => { + for (const mu of mutationList) { + if (mu.target.className === 'carouselContent-enter-done') { + this.setImgOnClickEventHelper(mu.target, rect) + break + } + } + }) + const imgList = markdownPreviewIframe.contentWindow.document.body.querySelectorAll('img') for (const img of imgList) { - img.onclick = () => { - const widthMagnification = document.body.clientWidth / img.width - const heightMagnification = document.body.clientHeight / img.height - const baseOnWidth = widthMagnification < heightMagnification - const magnification = baseOnWidth ? widthMagnification : heightMagnification + const parentEl = img.parentElement + this.setImgOnClickEventHelper(img, rect) + imgObserver.observe(parentEl, config) + } + } - const zoomImgWidth = img.width * magnification - const zoomImgHeight = img.height * magnification - const zoomImgTop = (document.body.clientHeight - zoomImgHeight) / 2 - const zoomImgLeft = (document.body.clientWidth - zoomImgWidth) / 2 - const originalImgTop = img.y + rect.top - const originalImgLeft = img.x + rect.left - const originalImgRect = { - top: `${originalImgTop}px`, - left: `${originalImgLeft}px`, - width: `${img.width}px`, - height: `${img.height}px` - } - const zoomInImgRect = { - top: `${baseOnWidth ? zoomImgTop : 0}px`, - left: `${baseOnWidth ? 0 : zoomImgLeft}px`, - width: `${zoomImgWidth}px`, - height: `${zoomImgHeight}px` - } - const animationSpeed = 300 + setImgOnClickEventHelper (img, rect) { + img.onclick = () => { + const widthMagnification = document.body.clientWidth / img.width + const heightMagnification = document.body.clientHeight / img.height + const baseOnWidth = widthMagnification < heightMagnification + const magnification = baseOnWidth ? widthMagnification : heightMagnification - const zoomImg = document.createElement('img') - zoomImg.src = img.src + const zoomImgWidth = img.width * magnification + const zoomImgHeight = img.height * magnification + const zoomImgTop = (document.body.clientHeight - zoomImgHeight) / 2 + const zoomImgLeft = (document.body.clientWidth - zoomImgWidth) / 2 + const originalImgTop = img.y + rect.top + const originalImgLeft = img.x + rect.left + const originalImgRect = { + top: `${originalImgTop}px`, + left: `${originalImgLeft}px`, + width: `${img.width}px`, + height: `${img.height}px` + } + const zoomInImgRect = { + top: `${baseOnWidth ? zoomImgTop : 0}px`, + left: `${baseOnWidth ? 0 : zoomImgLeft}px`, + width: `${zoomImgWidth}px`, + height: `${zoomImgHeight}px` + } + const animationSpeed = 300 + + const zoomImg = document.createElement('img') + zoomImg.src = img.src + zoomImg.style = ` + position: absolute; + top: ${baseOnWidth ? zoomImgTop : 0}px; + left: ${baseOnWidth ? 0 : zoomImgLeft}px; + width: ${zoomImgWidth}; + height: ${zoomImgHeight}px; + ` + zoomImg.animate([ + originalImgRect, + zoomInImgRect + ], animationSpeed) + + const overlay = document.createElement('div') + overlay.style = ` + background-color: rgba(0,0,0,0.5); + cursor: zoom-out; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: ${document.body.clientHeight}px; + z-index: 100; + ` + overlay.onclick = () => { zoomImg.style = ` position: absolute; - top: ${baseOnWidth ? zoomImgTop : 0}px; - left: ${baseOnWidth ? 0 : zoomImgLeft}px; - width: ${zoomImgWidth}; - height: ${zoomImgHeight}px; + top: ${originalImgTop}px; + left: ${originalImgLeft}px; + width: ${img.width}px; + height: ${img.height}px; ` - zoomImg.animate([ - originalImgRect, - zoomInImgRect + const zoomOutImgAnimation = zoomImg.animate([ + zoomInImgRect, + originalImgRect ], animationSpeed) - - const overlay = document.createElement('div') - overlay.style = ` - background-color: rgba(0,0,0,0.5); - cursor: zoom-out; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: ${document.body.clientHeight}px; - z-index: 100; - ` - overlay.onclick = () => { - zoomImg.style = ` - position: absolute; - top: ${originalImgTop}px; - left: ${originalImgLeft}px; - width: ${img.width}px; - height: ${img.height}px; - ` - const zoomOutImgAnimation = zoomImg.animate([ - zoomInImgRect, - originalImgRect - ], animationSpeed) - zoomOutImgAnimation.onfinish = () => overlay.remove() - } - - overlay.appendChild(zoomImg) - document.body.appendChild(overlay) + zoomOutImgAnimation.onfinish = () => overlay.remove() } + + overlay.appendChild(zoomImg) + document.body.appendChild(overlay) } + + this.getWindow().scrollTo(0, 0) } focus () { @@ -1006,9 +1015,11 @@ export default class MarkdownPreview extends React.Component { e.preventDefault() e.stopPropagation() - const href = e.target.href + const href = e.target.getAttribute('href') const linkHash = href.split('/').pop() + if (!href) return + const regexNoteInternalLink = /main.html#(.+)/ if (regexNoteInternalLink.test(linkHash)) { const targetId = mdurl.encode(linkHash.match(regexNoteInternalLink)[1]) diff --git a/browser/components/NavToggleButton.js b/browser/components/NavToggleButton.js index ad0ff54c..7dc75e90 100644 --- a/browser/components/NavToggleButton.js +++ b/browser/components/NavToggleButton.js @@ -16,8 +16,8 @@ const NavToggleButton = ({isFolded, handleToggleButtonClick}) => ( onClick={(e) => handleToggleButtonClick(e)} > {isFolded - ? - : + ? + : } ) diff --git a/browser/components/NavToggleButton.styl b/browser/components/NavToggleButton.styl index ae9dd6ca..422a7ca6 100644 --- a/browser/components/NavToggleButton.styl +++ b/browser/components/NavToggleButton.styl @@ -7,7 +7,7 @@ border-radius 16.5px height 34px width 34px - line-height 32px + line-height 100% padding 0 &:hover border: 1px solid #1EC38B; diff --git a/browser/lib/consts.js b/browser/lib/consts.js index 84b962eb..9c993055 100644 --- a/browser/lib/consts.js +++ b/browser/lib/consts.js @@ -3,14 +3,43 @@ const fs = require('sander') const { remote } = require('electron') const { app } = remote -const themePath = process.env.NODE_ENV === 'production' - ? path.join(app.getAppPath(), './node_modules/codemirror/theme') - : require('path').resolve('./node_modules/codemirror/theme') -const themes = fs.readdirSync(themePath) - .map((themePath) => { - return themePath.substring(0, themePath.lastIndexOf('.')) - }) -themes.splice(themes.indexOf('solarized'), 1, 'solarized dark', 'solarized light') +const CODEMIRROR_THEME_PATH = 'node_modules/codemirror/theme' +const CODEMIRROR_EXTRA_THEME_PATH = 'extra_scripts/codemirror/theme' + +const isProduction = process.env.NODE_ENV === 'production' +const paths = [ + isProduction ? path.join(app.getAppPath(), CODEMIRROR_THEME_PATH) : path.resolve(CODEMIRROR_THEME_PATH), + isProduction ? path.join(app.getAppPath(), CODEMIRROR_EXTRA_THEME_PATH) : path.resolve(CODEMIRROR_EXTRA_THEME_PATH) +] + +const themes = paths + .map(directory => fs.readdirSync(directory).map(file => { + const name = file.substring(0, file.lastIndexOf('.')) + + return { + name, + path: path.join(directory.split(/\//g).slice(-3).join('/'), file), + className: `cm-s-${name}` + } + })) + .reduce((accumulator, value) => accumulator.concat(value), []) + .sort((a, b) => a.name.localeCompare(b.name)) + +themes.splice(themes.findIndex(({ name }) => name === 'solarized'), 1, { + name: 'solarized dark', + path: `${CODEMIRROR_THEME_PATH}/solarized.css`, + className: `cm-s-solarized cm-s-dark` +}, { + name: 'solarized light', + path: `${CODEMIRROR_THEME_PATH}/solarized.css`, + className: `cm-s-solarized cm-s-light` +}) + +themes.splice(0, 0, { + name: 'default', + path: `${CODEMIRROR_THEME_PATH}/elegant.css`, + className: `cm-s-default` +}) const snippetFile = process.env.NODE_ENV !== 'test' ? path.join(app.getPath('userData'), 'snippets.json') @@ -35,7 +64,7 @@ const consts = { 'Dodger Blue', 'Violet Eggplant' ], - THEMES: ['default'].concat(themes), + THEMES: themes, SNIPPET_FILE: snippetFile, DEFAULT_EDITOR_FONT_FAMILY: [ 'Monaco', diff --git a/browser/lib/markdown-toc-generator.js b/browser/lib/markdown-toc-generator.js index af1c833f..8f027247 100644 --- a/browser/lib/markdown-toc-generator.js +++ b/browser/lib/markdown-toc-generator.js @@ -28,6 +28,8 @@ function linkify (token) { const TOC_MARKER_START = '' const TOC_MARKER_END = '' +const tocRegex = new RegExp(`${TOC_MARKER_START}[\\s\\S]*?${TOC_MARKER_END}`) + /** * Takes care of proper updating given editor with TOC. * If TOC doesn't exit in the editor, it's inserted at current caret position. @@ -35,12 +37,6 @@ const TOC_MARKER_END = '' * @param editor CodeMirror editor to be updated with TOC */ export function generateInEditor (editor) { - const tocRegex = new RegExp(`${TOC_MARKER_START}[\\s\\S]*?${TOC_MARKER_END}`) - - function tocExistsInEditor () { - return tocRegex.test(editor.getValue()) - } - function updateExistingToc () { const toc = generate(editor.getValue()) const search = editor.getSearchCursor(tocRegex) @@ -54,13 +50,17 @@ export function generateInEditor (editor) { editor.replaceRange(wrapTocWithEol(toc, editor), editor.getCursor()) } - if (tocExistsInEditor()) { + if (tocExistsInEditor(editor)) { updateExistingToc() } else { addTocAtCursorPosition() } } +export function tocExistsInEditor (editor) { + return tocRegex.test(editor.getValue()) +} + /** * Generates MD TOC based on MD document passed as string. * @param markdownText MD document @@ -94,5 +94,6 @@ function wrapTocWithEol (toc, editor) { export default { generate, - generateInEditor + generateInEditor, + tocExistsInEditor } diff --git a/browser/lib/markdown.js b/browser/lib/markdown.js index 0ea15ba9..df429b19 100644 --- a/browser/lib/markdown.js +++ b/browser/lib/markdown.js @@ -181,7 +181,7 @@ class Markdown { }) const deflate = require('markdown-it-plantuml/lib/deflate') - this.md.use(require('markdown-it-plantuml'), '', { + 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' diff --git a/browser/lib/utils.js b/browser/lib/utils.js index 1d15b722..4bcc9698 100644 --- a/browser/lib/utils.js +++ b/browser/lib/utils.js @@ -132,8 +132,13 @@ export function isObjectEqual (a, b) { return true } +export function isMarkdownTitleURL (str) { + return /(^#{1,6}\s)(?:\w+:|^)\/\/(?:[^\s\.]+\.\S{2}|localhost[\:?\d]*)/.test(str) +} + export default { lastFindInArray, escapeHtmlCharacters, - isObjectEqual + isObjectEqual, + isMarkdownTitleURL } diff --git a/browser/main/NoteList/index.js b/browser/main/NoteList/index.js index f0626fb9..cfcfcc99 100644 --- a/browser/main/NoteList/index.js +++ b/browser/main/NoteList/index.js @@ -895,7 +895,7 @@ class NoteList extends React.Component { if (!location.pathname.match(/\/trashed/)) this.addNotesFromFiles(filepaths) } - // Add notes to the current folder + // Add notes to the current folder addNotesFromFiles (filepaths) { const { dispatch, location } = this.props const { storage, folder } = this.resolveTargetFolder() @@ -919,13 +919,20 @@ class NoteList extends React.Component { } dataApi.createNote(storage.key, newNote) .then((note) => { - dispatch({ - type: 'UPDATE_NOTE', - note: note - }) - hashHistory.push({ - pathname: location.pathname, - query: {key: getNoteKey(note)} + attachmentManagement.importAttachments(note.content, filepath, storage.key, note.key) + .then((newcontent) => { + note.content = newcontent + + dataApi.updateNote(storage.key, note.key, note) + + dispatch({ + type: 'UPDATE_NOTE', + note: note + }) + hashHistory.push({ + pathname: location.pathname, + query: {key: getNoteKey(note)} + }) }) }) }) diff --git a/browser/main/lib/ConfigManager.js b/browser/main/lib/ConfigManager.js index 05f3d822..f20b3d88 100644 --- a/browser/main/lib/ConfigManager.js +++ b/browser/main/lib/ConfigManager.js @@ -132,16 +132,12 @@ function get () { document.head.appendChild(editorTheme) } - config.editor.theme = consts.THEMES.some((theme) => theme === config.editor.theme) - ? config.editor.theme - : 'default' + const theme = consts.THEMES.find(theme => theme.name === config.editor.theme) - if (config.editor.theme !== 'default') { - if (config.editor.theme.startsWith('solarized')) { - editorTheme.setAttribute('href', '../node_modules/codemirror/theme/solarized.css') - } else { - editorTheme.setAttribute('href', '../node_modules/codemirror/theme/' + config.editor.theme + '.css') - } + if (theme) { + editorTheme.setAttribute('href', `../${theme.path}`) + } else { + config.editor.theme = 'default' } } @@ -177,16 +173,11 @@ function set (updates) { editorTheme.setAttribute('rel', 'stylesheet') document.head.appendChild(editorTheme) } - const newTheme = consts.THEMES.some((theme) => theme === newConfig.editor.theme) - ? newConfig.editor.theme - : 'default' - if (newTheme !== 'default') { - if (newTheme.startsWith('solarized')) { - editorTheme.setAttribute('href', '../node_modules/codemirror/theme/solarized.css') - } else { - editorTheme.setAttribute('href', '../node_modules/codemirror/theme/' + newTheme + '.css') - } + const newTheme = consts.THEMES.find(theme => theme.name === newConfig.editor.theme) + + if (newTheme) { + editorTheme.setAttribute('href', `../${newTheme.path}`) } ipcRenderer.send('config-renew', { diff --git a/browser/main/lib/dataApi/attachmentManagement.js b/browser/main/lib/dataApi/attachmentManagement.js index 6fa3b51f..d92a1eb4 100644 --- a/browser/main/lib/dataApi/attachmentManagement.js +++ b/browser/main/lib/dataApi/attachmentManagement.js @@ -85,7 +85,7 @@ function getOrientation (file) { return view.getUint16(offset + (i * 12) + 8, little) } } - } else if ((marker & 0xFF00) !== 0xFF00) { // If not start with 0xFF, not a Marker + } else if ((marker & 0xFF00) !== 0xFF00) { // If not start with 0xFF, not a Marker. break } else { offset += view.getUint16(offset, false) @@ -278,27 +278,40 @@ function handleAttachmentDrop (codeEditor, storageKey, noteKey, dropEvent) { let promise if (dropEvent.dataTransfer.files.length > 0) { promise = Promise.all(Array.from(dropEvent.dataTransfer.files).map(file => { - if (file.type.startsWith('image')) { - if (file.type === 'image/gif' || file.type === 'image/svg+xml') { - return copyAttachment(file.path, storageKey, noteKey).then(fileName => ({ + const filePath = file.path + const fileType = file.type // EX) 'image/gif' or 'text/html' + if (fileType.startsWith('image')) { + if (fileType === 'image/gif' || fileType === 'image/svg+xml') { + return copyAttachment(filePath, storageKey, noteKey).then(fileName => ({ fileName, - title: path.basename(file.path), + title: path.basename(filePath), isImage: true })) } else { - return fixRotate(file) - .then(data => copyAttachment({type: 'base64', data: data, sourceFilePath: file.path}, storageKey, noteKey) - .then(fileName => ({ + return getOrientation(file) + .then((orientation) => { + if (orientation === -1) { // The image rotation is correct and does not need adjustment + return copyAttachment(filePath, storageKey, noteKey) + } else { + return fixRotate(file).then(data => copyAttachment({ + type: 'base64', + data: data, + sourceFilePath: filePath + }, storageKey, noteKey)) + } + }) + .then(fileName => + ({ fileName, - title: path.basename(file.path), + title: path.basename(filePath), isImage: true - })) + }) ) } } else { - return copyAttachment(file.path, storageKey, noteKey).then(fileName => ({ + return copyAttachment(filePath, storageKey, noteKey).then(fileName => ({ fileName, - title: path.basename(file.path), + title: path.basename(filePath), isImage: false })) } @@ -325,13 +338,18 @@ function handleAttachmentDrop (codeEditor, storageKey, noteKey, dropEvent) { canvas.height = image.height context.drawImage(image, 0, 0) - return copyAttachment({type: 'base64', data: canvas.toDataURL(), sourceFilePath: imageURL}, storageKey, noteKey) + return copyAttachment({ + type: 'base64', + data: canvas.toDataURL(), + sourceFilePath: imageURL + }, storageKey, noteKey) }) .then(fileName => ({ fileName, title: imageURL, isImage: true - }))]) + })) + ]) } promise.then(files => { @@ -449,6 +467,54 @@ function getAbsolutePathsOfAttachmentsInContent (markdownContent, storagePath) { return result } +/** + * @description Copies the attachments to the storage folder and returns the mardown content it should be replaced with + * @param {String} markDownContent content in which the attachment paths should be found + * @param {String} filepath The path of the file with attachments to import + * @param {String} storageKey Storage key of the destination storage + * @param {String} noteKey Key of the current note. Will be used as subfolder in :storage + */ +function importAttachments (markDownContent, filepath, storageKey, noteKey) { + return new Promise((resolve, reject) => { + const nameRegex = /(!\[.*?]\()(.+?\..+?)(\))/g + let attachPath = nameRegex.exec(markDownContent) + const promiseArray = [] + const attachmentPaths = [] + const groupIndex = 2 + + while (attachPath) { + let attachmentPath = attachPath[groupIndex] + attachmentPaths.push(attachmentPath) + attachmentPath = path.isAbsolute(attachmentPath) ? attachmentPath : path.join(path.dirname(filepath), attachmentPath) + promiseArray.push(this.copyAttachment(attachmentPath, storageKey, noteKey)) + attachPath = nameRegex.exec(markDownContent) + } + + let numResolvedPromises = 0 + + if (promiseArray.length === 0) { + resolve(markDownContent) + } + + for (let j = 0; j < promiseArray.length; j++) { + promiseArray[j] + .then((fileName) => { + const newPath = path.join(STORAGE_FOLDER_PLACEHOLDER, noteKey, fileName) + markDownContent = markDownContent.replace(attachmentPaths[j], newPath) + }) + .catch((e) => { + console.error('File does not exist in path: ' + attachmentPaths[j]) + }) + .finally(() => { + numResolvedPromises++ + if (numResolvedPromises === promiseArray.length) { + resolve(markDownContent) + } + }) + } + }) +} + /** * @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. @@ -656,6 +722,7 @@ module.exports = { handlePasteNativeImage, getAttachmentsInMarkdownContent, getAbsolutePathsOfAttachmentsInContent, + importAttachments, removeStorageAndNoteReferences, deleteAttachmentFolder, deleteAttachmentsNotPresentInNote, diff --git a/browser/main/modals/NewNoteModal.js b/browser/main/modals/NewNoteModal.js index a190602c..41c174cb 100644 --- a/browser/main/modals/NewNoteModal.js +++ b/browser/main/modals/NewNoteModal.js @@ -8,7 +8,7 @@ import { createMarkdownNote, createSnippetNote } from 'browser/lib/newNote' class NewNoteModal extends React.Component { constructor (props) { super(props) - + this.lock = false this.state = {} } @@ -22,9 +22,12 @@ class NewNoteModal extends React.Component { handleMarkdownNoteButtonClick (e) { const { storage, folder, dispatch, location, params, config } = this.props - createMarkdownNote(storage, folder, dispatch, location, params, config).then(() => { - setTimeout(this.props.close, 200) - }) + if (!this.lock) { + this.lock = true + createMarkdownNote(storage, folder, dispatch, location, params, config).then(() => { + setTimeout(this.props.close, 200) + }) + } } handleMarkdownNoteButtonKeyDown (e) { @@ -36,9 +39,12 @@ class NewNoteModal extends React.Component { handleSnippetNoteButtonClick (e) { const { storage, folder, dispatch, location, params, config } = this.props - createSnippetNote(storage, folder, dispatch, location, params, config).then(() => { - setTimeout(this.props.close, 200) - }) + if (!this.lock) { + this.lock = true + createSnippetNote(storage, folder, dispatch, location, params, config).then(() => { + setTimeout(this.props.close, 200) + }) + } } handleSnippetNoteButtonKeyDown (e) { diff --git a/browser/main/modals/PreferencesModal/ConfigTab.styl b/browser/main/modals/PreferencesModal/ConfigTab.styl index 255165ce..0e22833d 100644 --- a/browser/main/modals/PreferencesModal/ConfigTab.styl +++ b/browser/main/modals/PreferencesModal/ConfigTab.styl @@ -18,6 +18,14 @@ margin-bottom 15px margin-top 30px +.group-header--sub + @extend .group-header + margin-bottom 10px + +.group-header2--sub + @extend .group-header2 + margin-bottom 10px + .group-section margin-bottom 20px display flex @@ -148,10 +156,12 @@ body[data-theme="dark"] color $ui-dark-text-color .group-header + .group-header--sub color $ui-dark-text-color border-color $ui-dark-borderColor .group-header2 + .group-header2--sub color $ui-dark-text-color .group-section-control-input @@ -176,10 +186,12 @@ body[data-theme="solarized-dark"] color $ui-solarized-dark-text-color .group-header + .group-header--sub color $ui-solarized-dark-text-color border-color $ui-solarized-dark-borderColor .group-header2 + .group-header2--sub color $ui-solarized-dark-text-color .group-section-control-input @@ -203,10 +215,12 @@ body[data-theme="monokai"] color $ui-monokai-text-color .group-header + .group-header--sub color $ui-monokai-text-color border-color $ui-monokai-borderColor .group-header2 + .group-header2--sub color $ui-monokai-text-color .group-section-control-input @@ -230,10 +244,12 @@ body[data-theme="dracula"] color $ui-dracula-text-color .group-header + .group-header--sub color $ui-dracula-text-color border-color $ui-dracula-borderColor .group-header2 + .group-header2--sub color $ui-dracula-text-color .group-section-control-input diff --git a/browser/main/modals/PreferencesModal/Crowdfunding.js b/browser/main/modals/PreferencesModal/Crowdfunding.js index f94ee5ca..56bb6e34 100644 --- a/browser/main/modals/PreferencesModal/Crowdfunding.js +++ b/browser/main/modals/PreferencesModal/Crowdfunding.js @@ -22,18 +22,16 @@ class Crowdfunding extends React.Component { render () { return (
{i18n.__('Thank you for using Boostnote!')}
{i18n.__('We launched IssueHunt which is an issue-based crowdfunding / sourcing platform for open source projects.')}
{i18n.__('Anyone can put a bounty on not only a bug but also on OSS feature requests listed on IssueHunt. Collected funds will be distributed to project owners and contributors.')}
-{i18n.__('### Sustainable Open Source Ecosystem')}
+{i18n.__('We discussed about open-source ecosystem and IssueHunt concept with the Boostnote team repeatedly. We actually also discussed with Matz who father of Ruby.')}
{i18n.__('The original reason why we made IssueHunt was to reward our contributors of Boostnote project. We’ve got tons of Github stars and hundred of contributors in two years.')}
{i18n.__('We thought that it will be nice if we can pay reward for our contributors.')}
-{i18n.__('### We believe Meritocracy')}
+{i18n.__('We think developers who have skills and do great things must be rewarded properly.')}
{i18n.__('OSS projects are used in everywhere on the internet, but no matter how they great, most of owners of those projects need to have another job to sustain their living.')}
{i18n.__('It sometimes looks like exploitation.')}
diff --git a/browser/main/modals/PreferencesModal/Crowdfunding.styl b/browser/main/modals/PreferencesModal/Crowdfunding.styl index 6d72290b..d1d6fc9f 100644 --- a/browser/main/modals/PreferencesModal/Crowdfunding.styl +++ b/browser/main/modals/PreferencesModal/Crowdfunding.styl @@ -1,14 +1,8 @@ -@import('./Tab') +@import('./ConfigTab') -.root - padding 15px - white-space pre - line-height 1.4 - color alpha($ui-text-color, 90%) - width 100% - font-size 14px p font-size 16px + line-height 1.4 .cf-link height 35px diff --git a/browser/main/modals/PreferencesModal/InfoTab.js b/browser/main/modals/PreferencesModal/InfoTab.js index dafabb02..8682f62d 100644 --- a/browser/main/modals/PreferencesModal/InfoTab.js +++ b/browser/main/modals/PreferencesModal/InfoTab.js @@ -69,8 +69,7 @@ class InfoTab extends React.Component { render () { return (