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.__('Crowdfunding')}
+
{i18n.__('Crowdfunding')}

{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.__('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 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 (
- -
{i18n.__('Community')}
+
{i18n.__('Community')}
  • @@ -108,7 +107,7 @@ class InfoTab extends React.Component {
    -
    {i18n.__('About')}
    +
    {i18n.__('About')}
    @@ -143,7 +142,7 @@ class InfoTab extends React.Component {
    -
    {i18n.__('Analytics')}
    +
    {i18n.__('Analytics')}
    {i18n.__('Boostnote collects anonymous data for the sole purpose of improving the application, and strictly does not collect any personal information such the contents of your notes.')}
    {i18n.__('You can see how it works on ')} this.handleLinkClick(e)}>GitHub.

    diff --git a/browser/main/modals/PreferencesModal/InfoTab.styl b/browser/main/modals/PreferencesModal/InfoTab.styl index 44f2d9ae..c541c91c 100644 --- a/browser/main/modals/PreferencesModal/InfoTab.styl +++ b/browser/main/modals/PreferencesModal/InfoTab.styl @@ -1,16 +1,4 @@ -@import('./Tab') - -.root - padding 15px - white-space pre - line-height 1.4 - color alpha($ui-text-color, 90%) - width 100% - font-size 14px - -.top - text-align left - margin-bottom 20px +@import('./ConfigTab.styl') .icon-space margin 20px 0 @@ -45,13 +33,21 @@ .separate-line margin 40px 0 -.policy - width 100% - font-size 20px - margin-bottom 10px - .policy-submit margin-top 10px + height 35px + border-radius 2px + border none + background-color alpha(#1EC38B, 90%) + padding-left 20px + padding-right 20px + text-decoration none + color white + font-weight 600 + font-size 16px + &:hover + background-color #1EC38B + transition 0.2s .policy-confirm margin-top 10px @@ -60,11 +56,14 @@ body[data-theme="dark"] .root color alpha($tab--dark-text-color, 80%) - + .appId + color $ui-dark-text-color body[data-theme="solarized-dark"] .root color $ui-solarized-dark-text-color + .appId + color $ui-solarized-dark-text-color .list a color $ui-solarized-dark-active-color @@ -72,6 +71,8 @@ body[data-theme="solarized-dark"] body[data-theme="monokai"] .root color $ui-monokai-text-color + .appId + color $ui-monokai-text-color .list a color $ui-monokai-active-color @@ -79,6 +80,8 @@ body[data-theme="monokai"] body[data-theme="dracula"] .root color $ui-dracula-text-color + .appId + color $ui-dracula-text-color .list a - color $ui-dracula-active-color + color $ui-dracula-active-color \ No newline at end of file diff --git a/browser/main/modals/PreferencesModal/SnippetTab.js b/browser/main/modals/PreferencesModal/SnippetTab.js index 5f5b0aac..df338d7f 100644 --- a/browser/main/modals/PreferencesModal/SnippetTab.js +++ b/browser/main/modals/PreferencesModal/SnippetTab.js @@ -91,7 +91,7 @@ class SnippetTab extends React.Component { if (!(editorFontSize > 0 && editorFontSize < 132)) editorIndentSize = 4 return (
    -
    {i18n.__('Snippets')}
    +
    {i18n.__('Snippets')}
    theme.name === newCodemirrorTheme) + + if (theme) { + checkHighLight.setAttribute('href', `../${theme.path}`) + } } + this.setState({ config: newConfig, codemirrorTheme: newCodemirrorTheme }, () => { const {ui, editor, preview} = this.props.config this.currentConfig = {ui, editor, preview} @@ -355,7 +360,7 @@ class UiTab extends React.Component { > { themes.map((theme) => { - return () + return () }) } @@ -670,7 +675,7 @@ class UiTab extends React.Component { > { themes.map((theme) => { - return () + return () }) } @@ -846,6 +851,7 @@ class UiTab extends React.Component { onChange={e => this.handleUIChange(e)} ref={e => (this.customCSSCM = e)} value={config.preview.customCSS} + defaultValue={'/* Drop Your Custom CSS Code Here */\n'} options={{ lineNumbers: true, mode: 'css', diff --git a/browser/main/store.js b/browser/main/store.js index 11ff2f3f..5edc115f 100644 --- a/browser/main/store.js +++ b/browser/main/store.js @@ -44,7 +44,9 @@ function data (state = defaultDataMap(), action) { const folderNoteSet = getOrInitItem(state.folderNoteMap, folderKey) folderNoteSet.add(uniqueKey) - assignToTags(note.tags, state, uniqueKey) + if (!note.isTrashed) { + assignToTags(note.tags, state, uniqueKey) + } }) return state case 'UPDATE_NOTE': diff --git a/extra_scripts/codemirror/theme/nord.css b/extra_scripts/codemirror/theme/nord.css new file mode 100644 index 00000000..c6b52e8a --- /dev/null +++ b/extra_scripts/codemirror/theme/nord.css @@ -0,0 +1,25 @@ +/* Theme: nord */ + +.cm-s-nord.CodeMirror { color: #d8dee9; } +.cm-s-nord.CodeMirror { background: #2e3440; } +.cm-s-nord .CodeMirror-cursor { color: #d8dee9; border-color: #d8dee9; } +.cm-s-nord .CodeMirror-activeline-background { background: #434c5e52 !important; } +.cm-s-nord .CodeMirror-selected { background: undefined; } +.cm-s-nord .cm-comment { color: #4c566a; } +.cm-s-nord .cm-string { color: #a3be8c; } +.cm-s-nord .cm-string-2 { color: #8fbcbb; } +.cm-s-nord .cm-property { color: #8fbcbb; } +.cm-s-nord .cm-qualifier { color: #8fbcbb; } +.cm-s-nord .cm-tag { color: #81a1c1; } +.cm-s-nord .cm-attribute { color: #8fbcbb; } +.cm-s-nord .cm-number { color: #b48ead; } +.cm-s-nord .cm-keyword { color: #81a1c1; } +.cm-s-nord .cm-operator { color: #81a1c1; } +.cm-s-nord .cm-error { background: #bf616a; color: #d8dee9; } +.cm-s-nord .cm-invalidchar { background: #bf616a; color: #d8dee9; } +.cm-s-nord .cm-variable { color: #d8dee9; } +.cm-s-nord .cm-variable-2 { color: #8fbcbb; } +.cm-s-nord .CodeMirror-gutters { + background: #3b4252; + color: #d8dee9; +} \ No newline at end of file diff --git a/gruntfile.js b/gruntfile.js index ec3bbf79..207f8685 100644 --- a/gruntfile.js +++ b/gruntfile.js @@ -39,7 +39,7 @@ module.exports = function (grunt) { name: 'boostnote', productName: 'Boostnote', genericName: 'Boostnote', - productDescription: 'The opensource note app for developer.', + productDescription: 'The opensource note app for developers.', arch: 'amd64', categories: [ 'Development', @@ -58,7 +58,7 @@ module.exports = function (grunt) { name: 'boostnote', productName: 'Boostnote', genericName: 'Boostnote', - productDescription: 'The opensource note app for developer.', + productDescription: 'The opensource note app for developers.', arch: 'x86_64', categories: [ 'Development', @@ -149,6 +149,7 @@ module.exports = function (grunt) { case 'osx': Object.assign(opts, { platform: 'darwin', + darwinDarkModeSupport: true, icon: path.join(__dirname, 'resources/app.icns'), 'app-category-type': 'public.app-category.developer-tools' }) diff --git a/lib/main.html b/lib/main.html index a1ea3610..87a88e2d 100644 --- a/lib/main.html +++ b/lib/main.html @@ -10,6 +10,7 @@ + Boostnote @@ -125,13 +126,15 @@ + + - + @@ -154,4 +157,4 @@ - \ No newline at end of file + diff --git a/locales/da.json b/locales/da.json index 130e9aa1..79503ab3 100644 --- a/locales/da.json +++ b/locales/da.json @@ -76,7 +76,7 @@ "Website": "Website", "Development": "Development", " : Development configurations for Boostnote.": " : Development configurations for Boostnote.", - "Copyright (C) 2017 - 2018 BoostIO": "Copyright (C) 2017 - 2018 BoostIO", + "Copyright (C) 2017 - 2019 BoostIO": "Copyright (C) 2017 - 2019 BoostIO", "License: GPL v3": "License: GPL v3", "Analytics": "Analytics", "Boostnote collects anonymous data for the sole purpose of improving the application, and strictly does not collect any personal information such the contents of your notes.": "Boostnote collects anonymous data for the sole purpose of improving the application, and strictly does not collect any personal information such the contents of your notes.", diff --git a/locales/de.json b/locales/de.json index 59c3d4ee..518a4e65 100644 --- a/locales/de.json +++ b/locales/de.json @@ -76,7 +76,7 @@ "Website": "Website", "Development": "Entwicklung", " : Development configurations for Boostnote.": " : Entwicklungseinstellungen für Boostnote.", - "Copyright (C) 2017 - 2018 BoostIO": "Copyright (C) 2017 - 2018 BoostIO", + "Copyright (C) 2017 - 2019 BoostIO": "Copyright (C) 2017 - 2019 BoostIO", "License: GPL v3": "License: GPL v3", "Analytics": "Analytics", "Boostnote collects anonymous data for the sole purpose of improving the application, and strictly does not collect any personal information such the contents of your notes.": "Boostnote sammelt anonyme Daten, um die App zu verbessern. Persönliche Informationen, wie z.B. der Inhalt deiner Notizen, werden dabei nicht erfasst.", diff --git a/locales/en.json b/locales/en.json index cca2589f..1e09bfc7 100644 --- a/locales/en.json +++ b/locales/en.json @@ -83,7 +83,7 @@ "Website": "Website", "Development": "Development", " : Development configurations for Boostnote.": " : Development configurations for Boostnote.", - "Copyright (C) 2017 - 2018 BoostIO": "Copyright (C) 2017 - 2018 BoostIO", + "Copyright (C) 2017 - 2019 BoostIO": "Copyright (C) 2017 - 2019 BoostIO", "License: GPL v3": "License: GPL v3", "Analytics": "Analytics", "Boostnote collects anonymous data for the sole purpose of improving the application, and strictly does not collect any personal information such the contents of your notes.": "Boostnote collects anonymous data for the sole purpose of improving the application, and strictly does not collect any personal information such the contents of your notes.", diff --git a/locales/es-ES.json b/locales/es-ES.json index 16d79b5c..a42e6bb4 100644 --- a/locales/es-ES.json +++ b/locales/es-ES.json @@ -76,7 +76,7 @@ "Website": "Página web", "Development": "Desarrollo", " : Development configurations for Boostnote.": " : Configuraciones de desarrollo para Boostnote.", - "Copyright (C) 2017 - 2018 BoostIO": "Copyright (C) 2017 - 2018 BoostIO", + "Copyright (C) 2017 - 2019 BoostIO": "Copyright (C) 2017 - 2019 BoostIO", "License: GPL v3": "Licencia: GPL v3", "Analytics": "Analítica", "Boostnote collects anonymous data for the sole purpose of improving the application, and strictly does not collect any personal information such the contents of your notes.": "Boostnote recopila datos anónimos con el único propósito de mejorar la aplicación, y de ninguna manera recopila información personal como el contenido de sus notas.", diff --git a/locales/fa.json b/locales/fa.json index e081b24b..d29e0e75 100644 --- a/locales/fa.json +++ b/locales/fa.json @@ -76,7 +76,7 @@ "Website": "وبسایت", "Development": "توسعه", " : Development configurations for Boostnote.": " : پیکربندی توسعه برای Boostnote.", - "Copyright (C) 2017 - 2018 BoostIO": "Copyright (C) 2017 - 2018 BoostIO", + "Copyright (C) 2017 - 2019 BoostIO": "Copyright (C) 2017 - 2019 BoostIO", "License: GPL v3": "لایسنس: GPL v3", "Analytics": "تجزیه و تحلیل", "Boostnote collects anonymous data for the sole purpose of improving the application, and strictly does not collect any personal information such the contents of your notes.": "Bosstnote اطلاعات ناشناس را برای بهبود عملکرد برنامه جمع آوری می‌کند.اطلاعات شخصی شما مثل محتوای یادداشت ها هرگز برای هیچ هدفی جمع آوری نمی‌شوند", diff --git a/locales/fr.json b/locales/fr.json index 53f4191d..c44b057e 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -77,7 +77,7 @@ "Website": "Site web", "Development": "Développement", " : Development configurations for Boostnote.": " : Configurations de développement pour Boostnote.", - "Copyright (C) 2017 - 2018 BoostIO": "Copyright (C) 2017 - 2018 BoostIO", + "Copyright (C) 2017 - 2019 BoostIO": "Copyright (C) 2017 - 2019 BoostIO", "License: GPL v3": "License: GPL v3", "Analytics": "Analytics", "Boostnote collects anonymous data for the sole purpose of improving the application, and strictly does not collect any personal information such the contents of your notes.": "Boostnote collecte des données anonymisées dans le seul but d'améliorer l'application, et ne collecte aucune donnée personnelle telle que le contenu de vos notes.", diff --git a/locales/hu.json b/locales/hu.json index e0206ccd..558770b9 100644 --- a/locales/hu.json +++ b/locales/hu.json @@ -82,7 +82,7 @@ "Website": "Weboldal", "Development": "Fejlesztés", " : Development configurations for Boostnote.": " : Információk a Boostnote fejlesztéséről.", - "Copyright (C) 2017 - 2018 BoostIO": "Szerzői jog (C) 2017 - 2018 BoostIO", + "Copyright (C) 2017 - 2019 BoostIO": "Szerzői jog (C) 2017 - 2019 BoostIO", "License: GPL v3": "Licensz: GPL v3", "Analytics": "Adatok elemzése", "Boostnote collects anonymous data for the sole purpose of improving the application, and strictly does not collect any personal information such the contents of your notes.": "A Boostnote névtelen adatokat gyűjt össze az alkalmazás tökéletesítése céljából, és szigorúan nem gyűjt semmilyen személyes adatot, például a jegyzetek tartalmát.", diff --git a/locales/it.json b/locales/it.json index 6f3c082f..3b070197 100644 --- a/locales/it.json +++ b/locales/it.json @@ -76,7 +76,7 @@ "Website": "Sito Web", "Development": "Sviluppo", " : Development configurations for Boostnote.": " : Configurazioni di sviluppo per Boostnote.", - "Copyright (C) 2017 - 2018 BoostIO": "Copyright (C) 2017 - 2018 BoostIO", + "Copyright (C) 2017 - 2019 BoostIO": "Copyright (C) 2017 - 2019 BoostIO", "License: GPL v3": "Licenza: GPL v3", "Analytics": "Statistiche", "Boostnote collects anonymous data for the sole purpose of improving the application, and strictly does not collect any personal information such the contents of your notes.": "Boostnote raccoglie dati anonimi al solo scopo di migliorare l'applicazione, e non raccoglie nessuna informazione personale rigurado il contenuto delle note.", diff --git a/locales/ja.json b/locales/ja.json index 34d248bc..087bce36 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -106,7 +106,7 @@ "Website": "ウェブサイト", "Development": "開発", " : Development configurations for Boostnote.": " : Boostnote の開発構成", - "Copyright (C) 2017 - 2018 BoostIO": "Copyright (C) 2017 - 2018 BoostIO", + "Copyright (C) 2017 - 2019 BoostIO": "Copyright (C) 2017 - 2019 BoostIO", "License: GPL v3": "ライセンス: GPL v3", "Analytics": "解析", "Boostnote collects anonymous data for the sole purpose of improving the application, and strictly does not collect any personal information such the contents of your notes.": "Boostnote はアプリケーションの機能向上だけを目的に匿名データを収集します。ノートの内容を含めた個人の情報は一切収集しません。", diff --git a/locales/ko.json b/locales/ko.json index ff256d14..3dbb1ada 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -76,7 +76,7 @@ "Website": "웹사이트", "Development": "개발", " : Development configurations for Boostnote.": " : Boostnote 개발을 위한 설정들.", - "Copyright (C) 2017 - 2018 BoostIO": "Copyright (C) 2017 - 2018 BoostIO", + "Copyright (C) 2017 - 2019 BoostIO": "Copyright (C) 2017 - 2019 BoostIO", "License: GPL v3": "License: GPL v3", "Analytics": "사용 통계/분석", "Boostnote collects anonymous data for the sole purpose of improving the application, and strictly does not collect any personal information such the contents of your notes.": "Boostnote는 서비스개선을 위해 익명으로 데이터를 수집하며 노트의 내용과같은 일체의 개인정보는 수집하지 않습니다.", diff --git a/locales/no.json b/locales/no.json index fe7e40b7..ff858153 100644 --- a/locales/no.json +++ b/locales/no.json @@ -76,7 +76,7 @@ "Website": "Website", "Development": "Development", " : Development configurations for Boostnote.": " : Development configurations for Boostnote.", - "Copyright (C) 2017 - 2018 BoostIO": "Copyright (C) 2017 - 2018 BoostIO", + "Copyright (C) 2017 - 2019 BoostIO": "Copyright (C) 2017 - 2019 BoostIO", "License: GPL v3": "License: GPL v3", "Analytics": "Analytics", "Boostnote collects anonymous data for the sole purpose of improving the application, and strictly does not collect any personal information such the contents of your notes.": "Boostnote collects anonymous data for the sole purpose of improving the application, and strictly does not collect any personal information such the contents of your notes.", diff --git a/locales/pl.json b/locales/pl.json index cb0aaebc..ffdc14be 100644 --- a/locales/pl.json +++ b/locales/pl.json @@ -82,7 +82,7 @@ "Website": "Strona WWW", "Development": "Development", " : Development configurations for Boostnote.": " : Development configurations for Boostnote.", - "Copyright (C) 2017 - 2018 BoostIO": "Copyright (C) 2017 - 2018 BoostIO", + "Copyright (C) 2017 - 2019 BoostIO": "Copyright (C) 2017 - 2019 BoostIO", "License: GPL v3": "Licencja: GPL v3", "Analytics": "Statystyki", "Boostnote collects anonymous data for the sole purpose of improving the application, and strictly does not collect any personal information such the contents of your notes.": "Boostnote zbiera anonimowe dane wyłącznie w celu poprawy działania aplikacji, lecz nigdy nie pobiera prywatnych danych z Twoich notatek.", diff --git a/locales/pt-BR.json b/locales/pt-BR.json index b78e0fce..ada02453 100644 --- a/locales/pt-BR.json +++ b/locales/pt-BR.json @@ -76,7 +76,7 @@ "Website": "Website", "Development": "Desenvolvimento", " : Development configurations for Boostnote.": " : Configurações de desenvolvimento para o Boostnote.", - "Copyright (C) 2017 - 2018 BoostIO": "Direitos Autorais (C) 2017 - 2018 BoostIO", + "Copyright (C) 2017 - 2019 BoostIO": "Direitos Autorais (C) 2017 - 2019 BoostIO", "License: GPL v3": "Licença: GPL v3", "Analytics": "Técnicas analíticas", "Boostnote collects anonymous data for the sole purpose of improving the application, and strictly does not collect any personal information such the contents of your notes.": "O Boostnote coleta dados anônimos com o único propósito de melhorar o aplicativo e de modo algum coleta qualquer informação pessoal, bem como o conteúdo de suas anotações.", diff --git a/locales/pt-PT.json b/locales/pt-PT.json index 1ec2d3b7..159c2255 100644 --- a/locales/pt-PT.json +++ b/locales/pt-PT.json @@ -76,7 +76,7 @@ "Website": "Website", "Development": "Desenvolvimento", " : Development configurations for Boostnote.": " : Configurações de desenvolvimento para o Boostnote.", - "Copyright (C) 2017 - 2018 BoostIO": "Direitos de Autor (C) 2017 - 2018 BoostIO", + "Copyright (C) 2017 - 2019 BoostIO": "Direitos de Autor (C) 2017 - 2019 BoostIO", "License: GPL v3": "Licença: GPL v3", "Analytics": "Analíse de Data", "Boostnote collects anonymous data for the sole purpose of improving the application, and strictly does not collect any personal information such the contents of your notes.": "O Boostnote coleta dados anônimos com o único propósito de melhorar a aplicação e não adquire informação pessoal ou conteúdo das tuas notas.", diff --git a/locales/ru.json b/locales/ru.json index eab3eccc..70d140ce 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -75,7 +75,7 @@ "Website": "Сайт", "Development": "Разработка", " : Development configurations for Boostnote.": " : Разработческие конфигурации для Boostnote.", - "Copyright (C) 2017 - 2018 BoostIO": "Copyright (C) 2017 - 2018 BoostIO", + "Copyright (C) 2017 - 2019 BoostIO": "Copyright (C) 2017 - 2019 BoostIO", "License: GPL v3": "License: GPL v3", "Analytics": "Аналитика", "Boostnote collects anonymous data for the sole purpose of improving the application, and strictly does not collect any personal information such the contents of your notes.": "Boostnote собирает анонимные данные о пользовании приложением для того, чтобы улучшать пользовательский опыт. Мы не собираем личную информацию и содержание ваших записей.", diff --git a/locales/sq.json b/locales/sq.json index 0cf81368..33d8ec97 100644 --- a/locales/sq.json +++ b/locales/sq.json @@ -75,7 +75,7 @@ "Website": "Website", "Development": "Development", " : Development configurations for Boostnote.": " : Development configurations for Boostnote.", - "Copyright (C) 2017 - 2018 BoostIO": "Copyright (C) 2017 - 2018 BoostIO", + "Copyright (C) 2017 - 2019 BoostIO": "Copyright (C) 2017 - 2019 BoostIO", "License: GPL v3": "License: GPL v3", "Analytics": "Analytics", "Boostnote collects anonymous data for the sole purpose of improving the application, and strictly does not collect any personal information such the contents of your notes.": "Boostnote collects anonymous data for the sole purpose of improving the application, and strictly does not collect any personal information such the contents of your notes.", diff --git a/locales/th.json b/locales/th.json index 1ad8b7a2..ade52990 100644 --- a/locales/th.json +++ b/locales/th.json @@ -83,7 +83,7 @@ "Website": "เว็บไซต์", "Development": "การพัฒนา", " : Development configurations for Boostnote.": " : การตั้งค่าต่างๆสำหรับการพัฒนา Boostnote.", - "Copyright (C) 2017 - 2018 BoostIO": "สงวนลิขสิทธิ์ (C) 2017 - 2018 BoostIO", + "Copyright (C) 2017 - 2019 BoostIO": "สงวนลิขสิทธิ์ (C) 2017 - 2019 BoostIO", "License: GPL v3": "License: GPL v3", "Analytics": "การวิเคราะห์", "Boostnote collects anonymous data for the sole purpose of improving the application, and strictly does not collect any personal information such the contents of your notes.": "Boostnote จะเก็บข้อมูลแบบไม่ระบุตัวตนเพื่อนำไปใช้ในการปรับปรุงแอพพลิเคชันเท่านั้น, และจะไม่มีการเก็บข้อมูลส่วนตัวใดๆของคุณ เช่น ข้อมูลในโน๊ตของคุณอย่างเด็ดขาด.", diff --git a/locales/tr.json b/locales/tr.json index 3ff9104d..d9dd28f1 100644 --- a/locales/tr.json +++ b/locales/tr.json @@ -75,7 +75,7 @@ "Website": "Websitesi", "Development": "Geliştirme", " : Development configurations for Boostnote.": " : Boostnote için geliştirme ayarları.", - "Copyright (C) 2017 - 2018 BoostIO": "Her hakkı saklıdır. (C) 2017 - 2018 BoostIO", + "Copyright (C) 2017 - 2019 BoostIO": "Her hakkı saklıdır. (C) 2017 - 2019 BoostIO", "License: GPL v3": "Lisans: GPL v3", "Analytics": "Analizler", "Boostnote collects anonymous data for the sole purpose of improving the application, and strictly does not collect any personal information such the contents of your notes.": "Boostnote, uygulamanın geliştirilmesi amacıyla anonim veriler toplar. Notlarınızın içeriği gibi kişisel bilgiler kesinlikle toplanmaz.", diff --git a/locales/zh-CN.json b/locales/zh-CN.json index 30ffc3c7..76700a7f 100755 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -77,7 +77,7 @@ "Website": "官网", "Development": "开发", " : Development configurations for Boostnote.": " : Boostnote 的开发配置", - "Copyright (C) 2017 - 2018 BoostIO": "Copyright (C) 2017 - 2018 BoostIO", + "Copyright (C) 2017 - 2019 BoostIO": "Copyright (C) 2017 - 2019 BoostIO", "License: GPL v3": "License: GPL v3", "Analytics": "分析", "Boostnote collects anonymous data for the sole purpose of improving the application, and strictly does not collect any personal information such the contents of your notes.": "Boostnote 收集匿名数据只为了提升软件使用体验,绝对不收集任何个人信息(包括笔记内容)", diff --git a/locales/zh-TW.json b/locales/zh-TW.json index f718ece2..ec6fa80c 100755 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -75,7 +75,7 @@ "Website": "官網", "Development": "開發", " : Development configurations for Boostnote.": " : Boostnote 的開發組態", - "Copyright (C) 2017 - 2018 BoostIO": "Copyright (C) 2017 - 2018 BoostIO", + "Copyright (C) 2017 - 2019 BoostIO": "Copyright (C) 2017 - 2019 BoostIO", "License: GPL v3": "License: GPL v3", "Analytics": "分析", "Boostnote collects anonymous data for the sole purpose of improving the application, and strictly does not collect any personal information such the contents of your notes.": "Boostnote 收集匿名資料單純只為了提升軟體使用體驗,絕對不收集任何個人資料(包括筆記內容)", diff --git a/package.json b/package.json index a845c438..f00fb095 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "homepage": "https://boostnote.io", "dependencies": { "@enyaxu/markdown-it-anchor": "^5.0.2", + "@rokt33r/js-sequence-diagrams": "^2.0.6-2", "@rokt33r/markdown-it-math": "^4.0.1", "@rokt33r/season": "^5.3.0", "@susisu/mte-kernel": "^2.0.0", @@ -72,7 +73,6 @@ "iconv-lite": "^0.4.19", "immutable": "^3.8.1", "invert-color": "^2.0.0", - "js-sequence-diagrams": "^1000000.0.6", "js-yaml": "^3.12.0", "katex": "^0.9.0", "lodash": "^4.11.1", @@ -138,7 +138,7 @@ "devtron": "^1.1.0", "dom-storage": "^2.0.2", "electron": "3.0.8", - "electron-packager": "^12.0.0", + "electron-packager": "^12.2.0", "eslint": "^3.13.1", "eslint-config-standard": "^6.2.1", "eslint-config-standard-jsx": "^3.2.0", @@ -154,6 +154,7 @@ "jest-localstorage-mock": "^2.2.0", "jsdom": "^9.4.2", "json-loader": "^0.5.4", + "markdownlint": "^0.11.0", "merge-stream": "^1.0.0", "mock-require": "^3.0.1", "nib": "^1.1.0", diff --git a/tests/lib/utils.test.js b/tests/lib/utils.test.js new file mode 100644 index 00000000..a60cdfec --- /dev/null +++ b/tests/lib/utils.test.js @@ -0,0 +1,15 @@ +import { isMarkdownTitleURL } from '../../browser/lib/utils' + +describe('isMarkdownTitleURL', () => { + it('returns true for valid Markdown title with url', () => { + expect(isMarkdownTitleURL('# https://validurl.com')).toBe(true) + expect(isMarkdownTitleURL('## https://validurl.com')).toBe(true) + expect(isMarkdownTitleURL('###### https://validurl.com')).toBe(true) + }) + + it('returns true for invalid Markdown title with url', () => { + expect(isMarkdownTitleURL('1 https://validurl.com')).toBe(false) + expect(isMarkdownTitleURL('24 https://validurl.com')).toBe(false) + expect(isMarkdownTitleURL('####### https://validurl.com')).toBe(false) + }) +}) diff --git a/yarn.lock b/yarn.lock index c6fce749..8fa53631 100644 --- a/yarn.lock +++ b/yarn.lock @@ -71,6 +71,14 @@ pretty-ms "^0.2.1" text-table "^0.2.0" +"@rokt33r/js-sequence-diagrams@^2.0.6-2": + version "2.0.6-2" + resolved "https://registry.yarnpkg.com/@rokt33r/js-sequence-diagrams/-/js-sequence-diagrams-2.0.6-2.tgz#fe9c4ad8f70c356873739485d1eff5cf75008821" + integrity sha512-33oibMKJEqCyA83TBeRkc9ifBvoIi2pn/davZuW0PZNbgK7zBkZUdFz1yMPPksA0Rbrxapc9BOwU7xXIACmxhg== + dependencies: + raphael "~2.1.x" + underscore "~1.4.x" + "@rokt33r/markdown-it-math@^4.0.1": version "4.0.2" resolved "https://registry.yarnpkg.com/@rokt33r/markdown-it-math/-/markdown-it-math-4.0.2.tgz#87c7172f459833b05e406cfc846e0c0b7ebc24ef" @@ -2846,21 +2854,7 @@ electron-config@^1.0.0: dependencies: conf "^1.0.0" -electron-download@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/electron-download/-/electron-download-4.1.0.tgz#bf932c746f2f87ffcc09d1dd472f2ff6b9187845" - dependencies: - debug "^2.2.0" - env-paths "^1.0.0" - fs-extra "^2.0.0" - minimist "^1.2.0" - nugget "^2.0.0" - path-exists "^3.0.0" - rc "^1.1.2" - semver "^5.3.0" - sumchecker "^2.0.1" - -electron-download@^4.1.0: +electron-download@^4.1.0, electron-download@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/electron-download/-/electron-download-4.1.1.tgz#02e69556705cc456e520f9e035556ed5a015ebe8" dependencies: @@ -2921,13 +2915,13 @@ electron-osx-sign@^0.4.1: minimist "^1.2.0" plist "^2.1.0" -electron-packager@^12.0.0: - version "12.1.0" - resolved "https://registry.yarnpkg.com/electron-packager/-/electron-packager-12.1.0.tgz#048dd4ff3848be19c5873c315b5b312df6215328" +electron-packager@^12.2.0: + version "12.2.0" + resolved "https://registry.yarnpkg.com/electron-packager/-/electron-packager-12.2.0.tgz#e38e0702a12e5f62a00a03aabd0b9ad28aebab4b" dependencies: asar "^0.14.0" debug "^3.0.0" - electron-download "^4.0.0" + electron-download "^4.1.1" electron-osx-sign "^0.4.1" extract-zip "^1.0.3" fs-extra "^5.0.0" @@ -3824,13 +3818,6 @@ fs-extra@^1.0.0: jsonfile "^2.1.0" klaw "^1.0.0" -fs-extra@^2.0.0: - version "2.1.2" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-2.1.2.tgz#046c70163cef9aad46b0e4a7fa467fb22d71de35" - dependencies: - graceful-fs "^4.1.2" - jsonfile "^2.1.0" - fs-extra@^4.0.0, fs-extra@^4.0.1: version "4.0.3" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-4.0.3.tgz#0d852122e5bc5beb453fb028e9c0c9bf36340c94" @@ -5381,13 +5368,6 @@ js-queue@>=2.0.0: dependencies: easy-stack "^1.0.0" -js-sequence-diagrams@^1000000.0.6: - version "1000000.0.6" - resolved "https://registry.yarnpkg.com/js-sequence-diagrams/-/js-sequence-diagrams-1000000.0.6.tgz#e95db01420479c5ccbc12046af1da42fde649e5c" - dependencies: - raphael "~2.1.x" - underscore "~1.4.x" - js-string-escape@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/js-string-escape/-/js-string-escape-1.0.1.tgz#e2625badbc0d67c7533e9edc1068c587ae4137ef" @@ -6563,7 +6543,7 @@ npmlog@^4.0.2: gauge "~2.7.3" set-blocking "~2.0.0" -nugget@^2.0.0, nugget@^2.0.1: +nugget@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/nugget/-/nugget-2.0.1.tgz#201095a487e1ad36081b3432fa3cada4f8d071b0" dependencies: @@ -7459,6 +7439,7 @@ raphael@2.2.7, raphael@^2.2.7: raphael@~2.1.x: version "2.1.4" resolved "https://registry.yarnpkg.com/raphael/-/raphael-2.1.4.tgz#b09ca664ad048b814bb2ff5d4d1e75838cab9c97" + integrity sha1-sJymZK0Ei4FLsv9dTR51g4yrnJc= dependencies: eve "git://github.com/adobe-webplatform/eve.git#eef80ed" @@ -7471,7 +7452,7 @@ raw-body@2.3.2: iconv-lite "0.4.19" unpipe "1.0.0" -rc@^1.0.1, rc@^1.1.2, rc@^1.1.6, rc@^1.1.7, rc@^1.2.1: +rc@^1.0.1, rc@^1.1.6, rc@^1.1.7, rc@^1.2.1: version "1.2.8" resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" dependencies: @@ -8735,7 +8716,7 @@ stylus@^0.52.4: sax "0.5.x" source-map "0.1.x" -sumchecker@^2.0.1, sumchecker@^2.0.2: +sumchecker@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/sumchecker/-/sumchecker-2.0.2.tgz#0f42c10e5d05da5d42eea3e56c3399a37d6c5b3e" dependencies: @@ -9114,6 +9095,7 @@ underscore.string@~2.4.0: underscore@~1.4.x: version "1.4.4" resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.4.4.tgz#61a6a32010622afa07963bf325203cf12239d604" + integrity sha1-YaajIBBiKvoHljvzJSA88SI51gQ= underscore@~1.6.0: version "1.6.0"