diff --git a/.babelrc b/.babelrc index 270349d2..3a366286 100644 --- a/.babelrc +++ b/.babelrc @@ -7,7 +7,7 @@ "test": { "presets": ["env" ,"react", "es2015"], "plugins": [ - [ "babel-plugin-webpack-alias", { "config": "${PWD}/webpack.config.js" } ] + [ "babel-plugin-webpack-alias", { "config": "/webpack.config.js" } ] ] } } diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..ea304082 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +issuehunt: BoostIo/Boostnote diff --git a/.travis.yml b/.travis.yml index d9267f77..90548ee9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,10 @@ language: node_js node_js: - - 7 + - 8 script: - npm run lint && npm run test - yarn jest - - 'if [[ ${TRAVIS_PULL_REQUEST_BRANCH:-$TRAVIS_BRANCH} = "master" ]]; then npm install -g grunt npm@5.2 && grunt pre-build; fi' + - 'if [[ ${TRAVIS_PULL_REQUEST_BRANCH:-$TRAVIS_BRANCH} = "master" ]]; then npm install -g grunt npm@6.4 && grunt pre-build; fi' after_success: - openssl aes-256-cbc -K $encrypted_440d7f9a3c38_key -iv $encrypted_440d7f9a3c38_iv -in .snapcraft/travis_snapcraft.cfg -out .snapcraft/snapcraft.cfg -d diff --git a/browser/components/CodeEditor.js b/browser/components/CodeEditor.js index 556e2f7b..093752bb 100644 --- a/browser/components/CodeEditor.js +++ b/browser/components/CodeEditor.js @@ -14,18 +14,20 @@ import { import TextEditorInterface from 'browser/lib/TextEditorInterface' import eventEmitter from 'browser/main/lib/eventEmitter' import iconv from 'iconv-lite' -import crypto from 'crypto' -import consts from 'browser/lib/consts' + +import { isMarkdownTitleURL } from 'browser/lib/utils' import styles from '../components/CodeEditor.styl' -import fs from 'fs' const { ipcRenderer, remote, clipboard } = require('electron') import normalizeEditorFontFamily from 'browser/lib/normalizeEditorFontFamily' const spellcheck = require('browser/lib/spellcheck') const buildEditorContextMenu = require('browser/lib/contextMenuBuilder') import TurndownService from 'turndown' -import { - gfm -} from 'turndown-plugin-gfm' +import {languageMaps} from '../lib/CMLanguageList' +import snippetManager from '../lib/SnippetManager' +import {generateInEditor, tocExistsInEditor} from 'browser/lib/markdown-toc-generator' +import markdownlint from 'markdownlint' +import Jsonlint from 'jsonlint-mod' +import { DEFAULT_CONFIG } from '../main/lib/ConfigManager' CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js' @@ -38,85 +40,6 @@ function translateHotkey (hotkey) { return hotkey.replace(/\s*\+\s*/g, '-').replace(/Command/g, 'Cmd').replace(/Control/g, 'Ctrl') } -const languageMaps = { - brainfuck: 'Brainfuck', - cpp: 'C++', - cs: 'C#', - clojure: 'Clojure', - 'clojure-repl': 'ClojureScript', - cmake: 'CMake', - coffeescript: 'CoffeeScript', - crystal: 'Crystal', - css: 'CSS', - d: 'D', - dart: 'Dart', - delphi: 'Pascal', - diff: 'Diff', - django: 'Django', - dockerfile: 'Dockerfile', - ebnf: 'EBNF', - elm: 'Elm', - erlang: 'Erlang', - 'erlang-repl': 'Erlang', - fortran: 'Fortran', - fsharp: 'F#', - gherkin: 'Gherkin', - go: 'Go', - groovy: 'Groovy', - haml: 'HAML', - haskell: 'Haskell', - haxe: 'Haxe', - http: 'HTTP', - ini: 'toml', - java: 'Java', - javascript: 'JavaScript', - json: 'JSON', - julia: 'Julia', - kotlin: 'Kotlin', - less: 'LESS', - livescript: 'LiveScript', - lua: 'Lua', - markdown: 'Markdown', - mathematica: 'Mathematica', - nginx: 'Nginx', - nsis: 'NSIS', - objectivec: 'Objective-C', - ocaml: 'Ocaml', - perl: 'Perl', - php: 'PHP', - powershell: 'PowerShell', - properties: 'Properties files', - protobuf: 'ProtoBuf', - python: 'Python', - puppet: 'Puppet', - q: 'Q', - r: 'R', - ruby: 'Ruby', - rust: 'Rust', - sas: 'SAS', - scala: 'Scala', - scheme: 'Scheme', - scss: 'SCSS', - shell: 'Shell', - smalltalk: 'Smalltalk', - sml: 'SML', - sql: 'SQL', - stylus: 'Stylus', - swift: 'Swift', - tcl: 'Tcl', - tex: 'LaTex', - typescript: 'TypeScript', - twig: 'Twig', - vbnet: 'VB.NET', - vbscript: 'VBScript', - verilog: 'Verilog', - vhdl: 'VHDL', - xml: 'HTML', - xquery: 'XQuery', - yaml: 'YAML', - elixir: 'Elixir' -} - export default class CodeEditor extends React.Component { constructor (props) { super(props) @@ -163,6 +86,8 @@ export default class CodeEditor extends React.Component { this.searchHandler = (e, msg) => this.handleSearch(msg) this.searchState = null this.scrollToLineHandeler = this.scrollToLine.bind(this) + this.getCodeEditorLintConfig = this.getCodeEditorLintConfig.bind(this) + this.validatorOfMarkdown = this.validatorOfMarkdown.bind(this) this.formatTable = () => this.handleFormatTable() @@ -228,7 +153,8 @@ export default class CodeEditor extends React.Component { updateDefaultKeyMap () { const { hotkey } = this.props - const expandSnippet = this.expandSnippet.bind(this) + const self = this + const expandSnippet = snippetManager.expandSnippet this.defaultKeyMap = CodeMirror.normalizeKeyMap({ Tab: function (cm) { @@ -248,14 +174,16 @@ export default class CodeEditor extends React.Component { } cm.execCommand('goLineEnd') } else if ( - !charBeforeCursor.match(/\t|\s|\r|\n/) && + !charBeforeCursor.match(/\t|\s|\r|\n|\$/) && cursor.ch > 1 ) { // text expansion on tab key if the char before is alphabet - const snippets = JSON.parse( - fs.readFileSync(consts.SNIPPET_FILE, 'utf8') + const wordBeforeCursor = self.getWordBeforeCursor( + line, + cursor.line, + cursor.ch ) - if (expandSnippet(line, cursor, cm, snippets) === false) { + if (expandSnippet(wordBeforeCursor, cursor, cm) === false) { if (tabs) { cm.execCommand('insertTab') } else { @@ -277,6 +205,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') { @@ -307,25 +255,10 @@ export default class CodeEditor extends React.Component { } componentDidMount () { - const { rulers, enableRulers } = this.props + const { rulers, enableRulers, enableMarkdownLint } = this.props eventEmitter.on('line:jump', this.scrollToLineHandeler) - const defaultSnippet = [ - { - id: crypto.randomBytes(16).toString('hex'), - name: 'Dummy text', - prefix: ['lorem', 'ipsum'], - content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.' - } - ] - if (!fs.existsSync(consts.SNIPPET_FILE)) { - fs.writeFileSync( - consts.SNIPPET_FILE, - JSON.stringify(defaultSnippet, null, 4), - 'utf8' - ) - } - + snippetManager.init() this.updateDefaultKeyMap() this.value = this.props.value @@ -344,7 +277,8 @@ export default class CodeEditor extends React.Component { inputStyle: 'textarea', dragDrop: false, foldGutter: true, - gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'], + lint: enableMarkdownLint ? this.getCodeEditorLintConfig() : false, + gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter', 'CodeMirror-lint-markers'], autoCloseBrackets: { pairs: this.props.matchingPairs, triples: this.props.matchingTriples, @@ -354,6 +288,8 @@ export default class CodeEditor extends React.Component { extraKeys: this.defaultKeyMap }) + document.querySelector('.CodeMirror-lint-markers').style.display = enableMarkdownLint ? 'inline-block' : 'none' + if (!this.props.mode && this.props.value && this.props.autoDetect) { this.autoDetectLanguage(this.props.value) } else { @@ -520,61 +456,12 @@ export default class CodeEditor extends React.Component { this.initialHighlighting() } - expandSnippet (line, cursor, cm, snippets) { - const wordBeforeCursor = this.getWordBeforeCursor( - line, - cursor.line, - cursor.ch - ) - const templateCursorString = ':{}' - for (let i = 0; i < snippets.length; i++) { - if (snippets[i].prefix.indexOf(wordBeforeCursor.text) !== -1) { - if (snippets[i].content.indexOf(templateCursorString) !== -1) { - const snippetLines = snippets[i].content.split('\n') - let cursorLineNumber = 0 - let cursorLinePosition = 0 - - let cursorIndex - for (let j = 0; j < snippetLines.length; j++) { - cursorIndex = snippetLines[j].indexOf(templateCursorString) - - if (cursorIndex !== -1) { - cursorLineNumber = j - cursorLinePosition = cursorIndex - - break - } - } - - cm.replaceRange( - snippets[i].content.replace(templateCursorString, ''), - wordBeforeCursor.range.from, - wordBeforeCursor.range.to - ) - cm.setCursor({ - line: cursor.line + cursorLineNumber, - ch: cursorLinePosition + cursor.ch - wordBeforeCursor.text.length - }) - } else { - cm.replaceRange( - snippets[i].content, - wordBeforeCursor.range.from, - wordBeforeCursor.range.to - ) - } - return true - } - } - - return false - } - getWordBeforeCursor (line, lineNumber, cursorPosition) { let wordBeforeCursor = '' const originCursorPosition = cursorPosition - const emptyChars = /\t|\s|\r|\n/ + const emptyChars = /\t|\s|\r|\n|\$/ - // to prevent the word to expand is long that will crash the whole app + // to prevent the word is long that will crash the whole app // the safeStop is there to stop user to expand words that longer than 20 chars const safeStop = 20 @@ -584,7 +471,7 @@ export default class CodeEditor extends React.Component { if (!emptyChars.test(currentChar)) { wordBeforeCursor = currentChar + wordBeforeCursor } else if (wordBeforeCursor.length >= safeStop) { - throw new Error('Your snippet trigger is too long !') + throw new Error('Stopped after 20 loops for safety reason !') } else { break } @@ -629,7 +516,9 @@ export default class CodeEditor extends React.Component { let needRefresh = false const { rulers, - enableRulers + enableRulers, + enableMarkdownLint, + customMarkdownLintConfig } = this.props if (prevProps.mode !== this.props.mode) { this.setMode(this.props.mode) @@ -647,6 +536,16 @@ export default class CodeEditor extends React.Component { if (prevProps.keyMap !== this.props.keyMap) { needRefresh = true } + if (prevProps.enableMarkdownLint !== enableMarkdownLint || prevProps.customMarkdownLintConfig !== customMarkdownLintConfig) { + if (!enableMarkdownLint) { + this.editor.setOption('lint', {default: false}) + document.querySelector('.CodeMirror-lint-markers').style.display = 'none' + } else { + this.editor.setOption('lint', this.getCodeEditorLintConfig()) + document.querySelector('.CodeMirror-lint-markers').style.display = 'inline-block' + } + needRefresh = true + } if ( prevProps.enableRulers !== enableRulers || @@ -727,6 +626,56 @@ export default class CodeEditor extends React.Component { } } + getCodeEditorLintConfig () { + const { mode } = this.props + const checkMarkdownNoteIsOpen = mode === 'Boost Flavored Markdown' + + return checkMarkdownNoteIsOpen ? { + 'getAnnotations': this.validatorOfMarkdown, + 'async': true + } : false + } + + validatorOfMarkdown (text, updateLinting) { + const { customMarkdownLintConfig } = this.props + let lintConfigJson + try { + Jsonlint.parse(customMarkdownLintConfig) + lintConfigJson = JSON.parse(customMarkdownLintConfig) + } catch (err) { + eventEmitter.emit('APP_SETTING_ERROR') + return + } + const lintOptions = { + 'strings': { + 'content': text + }, + 'config': lintConfigJson + } + + return markdownlint(lintOptions, (err, result) => { + if (!err) { + const foundIssues = [] + const splitText = text.split('\n') + result.content.map(item => { + let ruleNames = '' + item.ruleNames.map((ruleName, index) => { + ruleNames += ruleName + ruleNames += (index === item.ruleNames.length - 1) ? ': ' : '/' + }) + const lineNumber = item.lineNumber - 1 + foundIssues.push({ + from: CodeMirror.Pos(lineNumber, 0), + to: CodeMirror.Pos(lineNumber, splitText[lineNumber].length), + message: ruleNames + item.ruleDescription, + severity: 'warning' + }) + }) + updateLinting(foundIssues) + } + }) + } + setMode (mode) { let syntax = CodeMirror.findModeByName(convertModeName(mode || 'text')) if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text') @@ -738,6 +687,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() @@ -746,6 +723,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 @@ -835,6 +818,9 @@ export default class CodeEditor extends React.Component { ch: 1 } this.editor.setCursor(cursor) + const top = this.editor.charCoords({line: num, ch: 0}, 'local').top + const middleHeight = this.editor.getScrollerElement().offsetHeight / 2 + this.editor.scrollTo(null, top - middleHeight - 5) } focus () { @@ -950,6 +936,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)) { @@ -991,7 +979,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 => { @@ -1003,22 +1001,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 => { @@ -1140,8 +1139,7 @@ export default class CodeEditor extends React.Component { } ref='root' tabIndex='-1' - style={ - { + style={{ fontFamily, fontSize: fontSize, width: width, @@ -1185,7 +1183,9 @@ CodeEditor.propTypes = { onChange: PropTypes.func, readOnly: PropTypes.bool, autoDetect: PropTypes.bool, - spellCheck: PropTypes.bool + spellCheck: PropTypes.bool, + enableMarkdownLint: PropTypes.bool, + customMarkdownLintConfig: PropTypes.string } CodeEditor.defaultProps = { @@ -1197,5 +1197,7 @@ CodeEditor.defaultProps = { indentSize: 4, indentType: 'space', autoDetect: false, - spellCheck: false + spellCheck: false, + enableMarkdownLint: DEFAULT_CONFIG.editor.enableMarkdownLint, + customMarkdownLintConfig: DEFAULT_CONFIG.editor.customMarkdownLintConfig } 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/MarkdownEditor.js b/browser/components/MarkdownEditor.js index 466be3fa..e956655c 100644 --- a/browser/components/MarkdownEditor.js +++ b/browser/components/MarkdownEditor.js @@ -32,6 +32,7 @@ class MarkdownEditor extends React.Component { componentDidMount () { this.value = this.refs.code.value eventEmitter.on('editor:lock', this.lockEditorCode) + eventEmitter.on('editor:focus', this.focusEditor.bind(this)) } componentDidUpdate () { @@ -47,6 +48,15 @@ class MarkdownEditor extends React.Component { componentWillUnmount () { this.cancelQueue() eventEmitter.off('editor:lock', this.lockEditorCode) + eventEmitter.off('editor:focus', this.focusEditor.bind(this)) + } + + focusEditor () { + this.setState({ + status: 'CODE' + }, () => { + this.refs.code.focus() + }) } queueRendering (value) { @@ -149,10 +159,10 @@ class MarkdownEditor extends React.Component { e.preventDefault() e.stopPropagation() const idMatch = /checkbox-([0-9]+)/ - const checkedMatch = /^\s*[\+\-\*] \[x\]/i - const uncheckedMatch = /^\s*[\+\-\*] \[ \]/ - const checkReplace = /\[x\]/i - const uncheckReplace = /\[ \]/ + const checkedMatch = /^(\s*>?)*\s*[+\-*] \[x]/i + const uncheckedMatch = /^(\s*>?)*\s*[+\-*] \[ ]/ + const checkReplace = /\[x]/i + const uncheckReplace = /\[ ]/ if (idMatch.test(e.target.getAttribute('id'))) { const lineIndex = parseInt(e.target.getAttribute('id').match(idMatch)[1], 10) - 1 const lines = this.refs.code.value @@ -309,6 +319,8 @@ class MarkdownEditor extends React.Component { enableSmartPaste={config.editor.enableSmartPaste} hotkey={config.hotkey} switchPreview={config.editor.switchPreview} + enableMarkdownLint={config.editor.enableMarkdownLint} + customMarkdownLintConfig={config.editor.customMarkdownLintConfig} /> this.handleSaveAsText() this.saveAsMdHandler = () => this.handleSaveAsMd() this.saveAsHtmlHandler = () => this.handleSaveAsHtml() + this.saveAsPdfHandler = () => this.handleSaveAsPdf() this.printHandler = () => this.handlePrint() this.linkClickHandler = this.handleLinkClick.bind(this) @@ -241,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) { @@ -268,17 +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.target != null) { - switch (e.target.tagName) { - case 'A': - case 'INPUT': - return null + if (e.ctrlKey) { + if (config.editor.type === 'SPLIT') { + if (lineNumber !== -1) { + eventEmitter.emit('line:jump', lineNumber) + } + } else { + if (lineNumber !== -1) { + eventEmitter.emit('editor:focus') + eventEmitter.emit('line:jump', lineNumber) + } } } - if (this.props.onMouseDown != null) this.props.onMouseDown(e) + + if (this.props.onMouseDown != null && targetTag === 'BODY') this.props.onMouseDown(e) } handleMouseUp (e) { @@ -297,58 +321,77 @@ export default class MarkdownPreview extends React.Component { this.exportAsDocument('md') } - handleSaveAsHtml () { - this.exportAsDocument('html', (noteContent, exportTasks) => { - const { - fontFamily, - fontSize, - codeBlockFontFamily, - lineNumber, - codeBlockTheme, - scrollPastEnd, - theme, - allowCustomCSS, - customCSS - } = this.getStyleParams() + htmlContentFormatter (noteContent, exportTasks, targetDir) { + const { + fontFamily, + fontSize, + codeBlockFontFamily, + lineNumber, + codeBlockTheme, + scrollPastEnd, + theme, + allowCustomCSS, + customCSS + } = this.getStyleParams() - const inlineStyles = buildStyle( - fontFamily, - fontSize, - codeBlockFontFamily, - lineNumber, - scrollPastEnd, - theme, - allowCustomCSS, - customCSS - ) - const body = this.markdown.render(noteContent) - const files = [this.GetCodeThemeLink(codeBlockTheme), ...CSS_FILES] - files.forEach(file => { - if (global.process.platform === 'win32') { - file = file.replace('file:///', '') - } else { - file = file.replace('file://', '') - } - exportTasks.push({ - src: file, - dst: 'css' + const inlineStyles = buildStyle( + fontFamily, + fontSize, + codeBlockFontFamily, + lineNumber, + scrollPastEnd, + theme, + allowCustomCSS, + customCSS + ) + const body = this.markdown.render(noteContent) + const files = [this.GetCodeThemeLink(codeBlockTheme), ...CSS_FILES] + files.forEach(file => { + if (global.process.platform === 'win32') { + file = file.replace('file:///', '') + } else { + file = file.replace('file://', '') + } + exportTasks.push({ + src: file, + dst: 'css' + }) + }) + + let styles = '' + files.forEach(file => { + styles += `` + }) + + return ` + + + + + + ${styles} + + ${body} + ` + } + + handleSaveAsHtml () { + this.exportAsDocument('html', (noteContent, exportTasks, targetDir) => Promise.resolve(this.htmlContentFormatter(noteContent, exportTasks, targetDir))) + } + + handleSaveAsPdf () { + this.exportAsDocument('pdf', (noteContent, exportTasks, targetDir) => { + const printout = new remote.BrowserWindow({show: false, webPreferences: {webSecurity: false}}) + printout.loadURL('data:text/html;charset=UTF-8,' + this.htmlContentFormatter(noteContent, exportTasks, targetDir)) + return new Promise((resolve, reject) => { + printout.webContents.on('did-finish-load', () => { + printout.webContents.printToPDF({}, (err, data) => { + if (err) reject(err) + else resolve(data) + printout.destroy() + }) }) }) - - let styles = '' - files.forEach(file => { - styles += `` - }) - - return ` - - - - - ${styles} - - ${body} - ` }) } @@ -490,6 +533,7 @@ export default class MarkdownPreview extends React.Component { eventEmitter.on('export:save-text', this.saveAsTextHandler) eventEmitter.on('export:save-md', this.saveAsMdHandler) eventEmitter.on('export:save-html', this.saveAsHtmlHandler) + eventEmitter.on('export:save-pdf', this.saveAsPdfHandler) eventEmitter.on('print', this.printHandler) } @@ -527,6 +571,7 @@ export default class MarkdownPreview extends React.Component { eventEmitter.off('export:save-text', this.saveAsTextHandler) eventEmitter.off('export:save-md', this.saveAsMdHandler) eventEmitter.off('export:save-html', this.saveAsHtmlHandler) + eventEmitter.off('export:save-pdf', this.saveAsPdfHandler) eventEmitter.off('print', this.printHandler) } @@ -625,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 () { @@ -690,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'), @@ -705,6 +750,8 @@ export default class MarkdownPreview extends React.Component { copyIcon.innerHTML = '' copyIcon.onclick = e => { + e.preventDefault() + e.stopPropagation() copy(content) if (showCopyNotification) { this.notify('Saved to Clipboard!', { @@ -713,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 }) @@ -835,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 () { @@ -953,13 +1015,19 @@ export default class MarkdownPreview extends React.Component { e.preventDefault() e.stopPropagation() - const href = e.target.href - const linkHash = href.split('/').pop() + const rawHref = e.target.getAttribute('href') + const parser = document.createElement('a') + parser.href = e.target.getAttribute('href') + const { href, hash } = parser + const linkHash = hash === '' ? rawHref : hash // needed because we're having special link formats that are removed by parser e.g. :line:10 - const regexNoteInternalLink = /main.html#(.+)/ - if (regexNoteInternalLink.test(linkHash)) { - const targetId = mdurl.encode(linkHash.match(regexNoteInternalLink)[1]) - const targetElement = this.refs.root.contentWindow.document.getElementById( + if (!rawHref) return // not checked href because parser will create file://... string for [empty link]() + + const regexNoteInternalLink = /.*[main.\w]*.html#/ + + if (regexNoteInternalLink.test(href)) { + const targetId = mdurl.encode(linkHash) + const targetElement = this.refs.root.contentWindow.document.querySelector( targetId ) diff --git a/browser/components/MarkdownSplitEditor.js b/browser/components/MarkdownSplitEditor.js index 0d71ab93..8912c289 100644 --- a/browser/components/MarkdownSplitEditor.js +++ b/browser/components/MarkdownSplitEditor.js @@ -79,10 +79,10 @@ class MarkdownSplitEditor extends React.Component { e.preventDefault() e.stopPropagation() const idMatch = /checkbox-([0-9]+)/ - const checkedMatch = /^\s*[\+\-\*] \[x\]/i - const uncheckedMatch = /^\s*[\+\-\*] \[ \]/ - const checkReplace = /\[x\]/i - const uncheckReplace = /\[ \]/ + const checkedMatch = /^(\s*>?)*\s*[+\-*] \[x]/i + const uncheckedMatch = /^(\s*>?)*\s*[+\-*] \[ ]/ + const checkReplace = /\[x]/i + const uncheckReplace = /\[ ]/ if (idMatch.test(e.target.getAttribute('id'))) { const lineIndex = parseInt(e.target.getAttribute('id').match(idMatch)[1], 10) - 1 const lines = this.refs.code.value @@ -224,6 +224,8 @@ class MarkdownSplitEditor extends React.Component { enableSmartPaste={config.editor.enableSmartPaste} hotkey={config.hotkey} switchPreview={config.editor.switchPreview} + enableMarkdownLint={config.editor.enableMarkdownLint} + customMarkdownLintConfig={config.editor.customMarkdownLintConfig} />
this.handleMouseDown(e)} >
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/CMLanguageList.js b/browser/lib/CMLanguageList.js new file mode 100644 index 00000000..0a8652a6 --- /dev/null +++ b/browser/lib/CMLanguageList.js @@ -0,0 +1,78 @@ +export const languageMaps = { + brainfuck: 'Brainfuck', + cpp: 'C++', + cs: 'C#', + clojure: 'Clojure', + 'clojure-repl': 'ClojureScript', + cmake: 'CMake', + coffeescript: 'CoffeeScript', + crystal: 'Crystal', + css: 'CSS', + d: 'D', + dart: 'Dart', + delphi: 'Pascal', + diff: 'Diff', + django: 'Django', + dockerfile: 'Dockerfile', + ebnf: 'EBNF', + elm: 'Elm', + erlang: 'Erlang', + 'erlang-repl': 'Erlang', + fortran: 'Fortran', + fsharp: 'F#', + gherkin: 'Gherkin', + go: 'Go', + groovy: 'Groovy', + haml: 'HAML', + haskell: 'Haskell', + haxe: 'Haxe', + http: 'HTTP', + ini: 'toml', + java: 'Java', + javascript: 'JavaScript', + json: 'JSON', + julia: 'Julia', + kotlin: 'Kotlin', + less: 'LESS', + livescript: 'LiveScript', + lua: 'Lua', + markdown: 'Markdown', + mathematica: 'Mathematica', + nginx: 'Nginx', + nsis: 'NSIS', + objectivec: 'Objective-C', + ocaml: 'Ocaml', + perl: 'Perl', + php: 'PHP', + powershell: 'PowerShell', + properties: 'Properties files', + protobuf: 'ProtoBuf', + python: 'Python', + puppet: 'Puppet', + q: 'Q', + r: 'R', + ruby: 'Ruby', + rust: 'Rust', + sas: 'SAS', + scala: 'Scala', + scheme: 'Scheme', + scss: 'SCSS', + shell: 'Shell', + smalltalk: 'Smalltalk', + sml: 'SML', + sql: 'SQL', + stylus: 'Stylus', + swift: 'Swift', + tcl: 'Tcl', + tex: 'LaTex', + typescript: 'TypeScript', + twig: 'Twig', + vbnet: 'VB.NET', + vbscript: 'VBScript', + verilog: 'Verilog', + vhdl: 'VHDL', + xml: 'HTML', + xquery: 'XQuery', + yaml: 'YAML', + elixir: 'Elixir' +} diff --git a/browser/lib/CSSModules.js b/browser/lib/CSSModules.js index 181274f4..691b44d2 100644 --- a/browser/lib/CSSModules.js +++ b/browser/lib/CSSModules.js @@ -1,5 +1,5 @@ import CSSModules from 'react-css-modules' export default function (component, styles) { - return CSSModules(component, styles, {errorWhenNotFound: false}) + return CSSModules(component, styles, {handleNotFoundStyleName: 'log'}) } diff --git a/browser/lib/Languages.js b/browser/lib/Languages.js index 8c3747a9..435747a9 100644 --- a/browser/lib/Languages.js +++ b/browser/lib/Languages.js @@ -62,10 +62,12 @@ const languages = [ { name: 'Spanish', locale: 'es-ES' - }, { + }, + { name: 'Turkish', locale: 'tr' - }, { + }, + { name: 'Thai', locale: 'th' } @@ -82,4 +84,3 @@ module.exports = { return languages } } - diff --git a/browser/lib/SnippetManager.js b/browser/lib/SnippetManager.js new file mode 100644 index 00000000..70f9b400 --- /dev/null +++ b/browser/lib/SnippetManager.js @@ -0,0 +1,91 @@ +import crypto from 'crypto' +import fs from 'fs' +import consts from './consts' + +class SnippetManager { + constructor () { + this.defaultSnippet = [ + { + id: crypto.randomBytes(16).toString('hex'), + name: 'Dummy text', + prefix: ['lorem', 'ipsum'], + content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.' + } + ] + this.snippets = [] + this.expandSnippet = this.expandSnippet.bind(this) + this.init = this.init.bind(this) + this.assignSnippets = this.assignSnippets.bind(this) + } + + init () { + if (fs.existsSync(consts.SNIPPET_FILE)) { + try { + this.snippets = JSON.parse( + fs.readFileSync(consts.SNIPPET_FILE, { encoding: 'UTF-8' }) + ) + } catch (error) { + console.log('Error while parsing snippet file') + } + return + } + fs.writeFileSync( + consts.SNIPPET_FILE, + JSON.stringify(this.defaultSnippet, null, 4), + 'utf8' + ) + this.snippets = this.defaultSnippet + } + + assignSnippets (snippets) { + this.snippets = snippets + } + + expandSnippet (wordBeforeCursor, cursor, cm) { + const templateCursorString = ':{}' + for (let i = 0; i < this.snippets.length; i++) { + if (this.snippets[i].prefix.indexOf(wordBeforeCursor.text) === -1) { + continue + } + if (this.snippets[i].content.indexOf(templateCursorString) !== -1) { + const snippetLines = this.snippets[i].content.split('\n') + let cursorLineNumber = 0 + let cursorLinePosition = 0 + + let cursorIndex + for (let j = 0; j < snippetLines.length; j++) { + cursorIndex = snippetLines[j].indexOf(templateCursorString) + + if (cursorIndex !== -1) { + cursorLineNumber = j + cursorLinePosition = cursorIndex + + break + } + } + + cm.replaceRange( + this.snippets[i].content.replace(templateCursorString, ''), + wordBeforeCursor.range.from, + wordBeforeCursor.range.to + ) + cm.setCursor({ + line: cursor.line + cursorLineNumber, + ch: cursorLinePosition + cursor.ch - wordBeforeCursor.text.length + }) + } else { + cm.replaceRange( + this.snippets[i].content, + wordBeforeCursor.range.from, + wordBeforeCursor.range.to + ) + } + return true + } + + return false + } +} + +const manager = new SnippetManager() +export default manager 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/getTodoStatus.js b/browser/lib/getTodoStatus.js index ab0d7809..8b552109 100644 --- a/browser/lib/getTodoStatus.js +++ b/browser/lib/getTodoStatus.js @@ -4,11 +4,11 @@ export function getTodoStatus (content) { let numberOfCompletedTodo = 0 splitted.forEach((line) => { - const trimmedLine = line.trim() - if (trimmedLine.match(/^[\+\-\*] \[(\s|x)\] ./i)) { + const trimmedLine = line.trim().replace(/^(>\s*)*/, '') + if (trimmedLine.match(/^[+\-*] \[(\s|x)] ./i)) { numberOfTodo++ } - if (trimmedLine.match(/^[\+\-\*] \[x\] ./i)) { + if (trimmedLine.match(/^[+\-*] \[x] ./i)) { numberOfCompletedTodo++ } }) diff --git a/browser/lib/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..5fd7c85c 100644 --- a/browser/lib/markdown.js +++ b/browser/lib/markdown.js @@ -32,6 +32,7 @@ class Markdown { const updatedOptions = Object.assign(defaultOptions, options) this.md = markdownit(updatedOptions) + this.md.linkify.set({ fuzzyLink: false }) if (updatedOptions.sanitize !== 'NONE') { const allowedTags = ['iframe', 'input', 'b', @@ -181,7 +182,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/newNote.js b/browser/lib/newNote.js index d8ef196f..4eec24f1 100644 --- a/browser/lib/newNote.js +++ b/browser/lib/newNote.js @@ -1,7 +1,8 @@ -import { hashHistory } from 'react-router' import dataApi from 'browser/main/lib/dataApi' import ee from 'browser/main/lib/eventEmitter' import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig' +import queryString from 'query-string' +import { push } from 'connected-react-router' export function createMarkdownNote (storage, folder, dispatch, location, params, config) { AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_MARKDOWN') @@ -28,10 +29,10 @@ export function createMarkdownNote (storage, folder, dispatch, location, params, note: note }) - hashHistory.push({ + dispatch(push({ pathname: location.pathname, - query: { key: noteHash } - }) + search: queryString.stringify({ key: noteHash }) + })) ee.emit('list:jump', noteHash) ee.emit('detail:focus') }) @@ -70,10 +71,10 @@ export function createSnippetNote (storage, folder, dispatch, location, params, type: 'UPDATE_NOTE', note: note }) - hashHistory.push({ + dispatch(push({ pathname: location.pathname, - query: { key: noteHash } - }) + search: queryString.stringify({ key: noteHash }) + })) ee.emit('list:jump', noteHash) ee.emit('detail:focus') }) diff --git a/browser/lib/spellcheck.js b/browser/lib/spellcheck.js index dd04e575..ab8fc81b 100644 --- a/browser/lib/spellcheck.js +++ b/browser/lib/spellcheck.js @@ -14,7 +14,7 @@ let self function getAvailableDictionaries () { return [ - {label: i18n.__('Disabled'), value: SPELLCHECK_DISABLED}, + {label: i18n.__('Spellcheck disabled'), value: SPELLCHECK_DISABLED}, {label: i18n.__('English'), value: 'en_GB'}, {label: i18n.__('German'), value: 'de_DE'}, {label: i18n.__('French'), value: 'fr_FR'} 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/Detail/InfoPanel.js b/browser/main/Detail/InfoPanel.js index 15535186..8fe0a855 100644 --- a/browser/main/Detail/InfoPanel.js +++ b/browser/main/Detail/InfoPanel.js @@ -14,7 +14,7 @@ class InfoPanel extends React.Component { render () { const { - storageName, folderName, noteLink, updatedAt, createdAt, exportAsMd, exportAsTxt, exportAsHtml, wordCount, letterCount, type, print + storageName, folderName, noteLink, updatedAt, createdAt, exportAsMd, exportAsTxt, exportAsHtml, exportAsPdf, wordCount, letterCount, type, print } = this.props return (
@@ -85,6 +85,11 @@ class InfoPanel extends React.Component {

{i18n.__('.html')}

+ + - @@ -61,7 +61,8 @@ InfoPanelTrashed.propTypes = { createdAt: PropTypes.string.isRequired, exportAsMd: PropTypes.func.isRequired, exportAsTxt: PropTypes.func.isRequired, - exportAsHtml: PropTypes.func.isRequired + exportAsHtml: PropTypes.func.isRequired, + exportAsPdf: PropTypes.func.isRequired } export default CSSModules(InfoPanelTrashed, styles) diff --git a/browser/main/Detail/MarkdownNoteDetail.js b/browser/main/Detail/MarkdownNoteDetail.js index 07c44d75..67a4d67c 100755 --- a/browser/main/Detail/MarkdownNoteDetail.js +++ b/browser/main/Detail/MarkdownNoteDetail.js @@ -9,7 +9,6 @@ import StarButton from './StarButton' import TagSelect from './TagSelect' import FolderSelect from './FolderSelect' import dataApi from 'browser/main/lib/dataApi' -import { hashHistory } from 'react-router' import ee from 'browser/main/lib/eventEmitter' import markdown from 'browser/lib/markdownTextHelper' import StatusBar from '../StatusBar' @@ -31,6 +30,8 @@ import { getTodoPercentageOfCompleted } from 'browser/lib/getTodoStatus' import striptags from 'striptags' import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote' import markdownToc from 'browser/lib/markdown-toc-generator' +import queryString from 'query-string' +import { replace } from 'connected-react-router' class MarkdownNoteDetail extends React.Component { constructor (props) { @@ -142,6 +143,7 @@ class MarkdownNoteDetail extends React.Component { } handleFolderChange (e) { + const { dispatch } = this.props const { note } = this.state const value = this.refs.folder.value const splitted = value.split('-') @@ -161,12 +163,12 @@ class MarkdownNoteDetail extends React.Component { originNote: note, note: newNote }) - hashHistory.replace({ + dispatch(replace({ pathname: location.pathname, - query: { + search: queryString.stringify({ key: newNote.key - } - }) + }) + })) this.setState({ isMovingNote: false }) @@ -203,6 +205,10 @@ class MarkdownNoteDetail extends React.Component { ee.emit('export:save-html') } + exportAsPdf () { + ee.emit('export:save-pdf') + } + handleKeyDown (e) { switch (e.keyCode) { // tab key @@ -438,6 +444,7 @@ class MarkdownNoteDetail extends React.Component { exportAsHtml={this.exportAsHtml} exportAsMd={this.exportAsMd} exportAsTxt={this.exportAsTxt} + exportAsPdf={this.exportAsPdf} />
@@ -503,12 +510,13 @@ class MarkdownNoteDetail extends React.Component { button, div + -webkit-user-drag none + user-select none + > img, span + -webkit-user-drag none + user-select none diff --git a/browser/main/Detail/SnippetNoteDetail.js b/browser/main/Detail/SnippetNoteDetail.js index 11d8ac2a..7503addb 100644 --- a/browser/main/Detail/SnippetNoteDetail.js +++ b/browser/main/Detail/SnippetNoteDetail.js @@ -8,7 +8,6 @@ import StarButton from './StarButton' import TagSelect from './TagSelect' import FolderSelect from './FolderSelect' import dataApi from 'browser/main/lib/dataApi' -import {hashHistory} from 'react-router' import ee from 'browser/main/lib/eventEmitter' import CodeMirror from 'codemirror' import 'codemirror-mode-elixir' @@ -18,7 +17,6 @@ import context from 'browser/lib/context' import ConfigManager from 'browser/main/lib/ConfigManager' import _ from 'lodash' import {findNoteTitle} from 'browser/lib/findNoteTitle' -import convertModeName from 'browser/lib/convertModeName' import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig' import FullscreenButton from './FullscreenButton' import TrashButton from './TrashButton' @@ -31,6 +29,8 @@ import { formatDate } from 'browser/lib/date-formatter' import i18n from 'browser/lib/i18n' import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote' import markdownToc from 'browser/lib/markdown-toc-generator' +import queryString from 'query-string' +import { replace } from 'connected-react-router' const electron = require('electron') const { remote } = electron @@ -166,12 +166,12 @@ class SnippetNoteDetail extends React.Component { originNote: note, note: newNote }) - hashHistory.replace({ + dispatch(replace({ pathname: location.pathname, - query: { + search: queryString.stringify({ key: newNote.key - } - }) + }) + })) this.setState({ isMovingNote: false }) @@ -657,6 +657,7 @@ class SnippetNoteDetail extends React.Component { 'export-txt': 'Text export', 'export-md': 'Markdown export', 'export-html': 'HTML export', + 'export-pdf': 'PDF export', 'print': 'Print' })[msg] @@ -770,6 +771,7 @@ class SnippetNoteDetail extends React.Component { exportAsMd={this.showWarning} exportAsTxt={this.showWarning} exportAsHtml={this.showWarning} + exportAsPdf={this.showWarning} />
@@ -812,12 +814,13 @@ class SnippetNoteDetail extends React.Component { diff --git a/browser/main/Detail/index.js b/browser/main/Detail/index.js index b6b6ef14..0ed3dd54 100644 --- a/browser/main/Detail/index.js +++ b/browser/main/Detail/index.js @@ -10,6 +10,7 @@ import StatusBar from '../StatusBar' import i18n from 'browser/lib/i18n' import debounceRender from 'react-debounce-render' import searchFromNotes from 'browser/lib/search' +import queryString from 'query-string' const OSX = global.process.platform === 'darwin' @@ -36,11 +37,11 @@ class Detail extends React.Component { } render () { - const { location, data, params, config } = this.props + const { location, data, match: { params }, config } = this.props + const noteKey = location.search !== '' && queryString.parse(location.search).key let note = null - if (location.query.key != null) { - const noteKey = location.query.key + if (location.search !== '') { const allNotes = data.noteMap.map(note => note) const trashedNotes = data.trashedSet.toJS().map(uniqueKey => data.noteMap.get(uniqueKey)) let displayedNotes = allNotes diff --git a/browser/main/DevTools/index.dev.js b/browser/main/DevTools/index.dev.js new file mode 100644 index 00000000..77d3eccd --- /dev/null +++ b/browser/main/DevTools/index.dev.js @@ -0,0 +1,16 @@ +import React from 'react' +import { createDevTools } from 'redux-devtools' +import LogMonitor from 'redux-devtools-log-monitor' +import DockMonitor from 'redux-devtools-dock-monitor' + +const DevTools = createDevTools( + + + +) + +export default DevTools diff --git a/browser/main/DevTools/index.js b/browser/main/DevTools/index.js new file mode 100644 index 00000000..93d666a2 --- /dev/null +++ b/browser/main/DevTools/index.js @@ -0,0 +1,8 @@ +/* eslint-disable no-undef */ +if (process.env.NODE_ENV === 'production') { + // eslint-disable-next-line global-require + module.exports = require('./index.prod').default +} else { + // eslint-disable-next-line global-require + module.exports = require('./index.dev').default +} diff --git a/browser/main/DevTools/index.prod.js b/browser/main/DevTools/index.prod.js new file mode 100644 index 00000000..762cae2c --- /dev/null +++ b/browser/main/DevTools/index.prod.js @@ -0,0 +1,6 @@ +import React from 'react' + +const DevTools = () =>
+DevTools.instrument = () => {} + +export default DevTools diff --git a/browser/main/Main.js b/browser/main/Main.js index 26fc8377..30bf8e8a 100644 --- a/browser/main/Main.js +++ b/browser/main/Main.js @@ -12,11 +12,11 @@ import _ from 'lodash' import ConfigManager from 'browser/main/lib/ConfigManager' import mobileAnalytics from 'browser/main/lib/AwsMobileAnalyticsConfig' import eventEmitter from 'browser/main/lib/eventEmitter' -import { hashHistory } from 'react-router' -import store from 'browser/main/store' +import { store } from 'browser/main/store' import i18n from 'browser/lib/i18n' import { getLocales } from 'browser/lib/Languages' import applyShortcuts from 'browser/main/lib/shortcutManager' +import { push } from 'connected-react-router' const path = require('path') const electron = require('electron') const { remote } = electron @@ -132,7 +132,7 @@ class Main extends React.Component { .then(() => data.storage) }) .then(storage => { - hashHistory.push('/storages/' + storage.key) + store.dispatch(push('/storages/' + storage.key)) }) .catch(err => { throw err @@ -311,7 +311,7 @@ class Main extends React.Component { onMouseUp={e => this.handleMouseUp(e)} > {!config.isSideNavFolded && @@ -341,7 +341,7 @@ class Main extends React.Component { 'dispatch', 'config', 'data', - 'params', + 'match', 'location' ])} /> @@ -351,7 +351,7 @@ class Main extends React.Component { 'dispatch', 'data', 'config', - 'params', + 'match', 'location' ])} /> @@ -373,7 +373,7 @@ class Main extends React.Component { 'dispatch', 'data', 'config', - 'params', + 'match', 'location' ])} ignorePreviewPointerEvents={this.state.isRightSliderFocused} diff --git a/browser/main/NewNoteButton/index.js b/browser/main/NewNoteButton/index.js index c34443be..115d9530 100644 --- a/browser/main/NewNoteButton/index.js +++ b/browser/main/NewNoteButton/index.js @@ -21,23 +21,20 @@ class NewNoteButton extends React.Component { this.state = { } - this.newNoteHandler = () => { - this.handleNewNoteButtonClick() - } + this.handleNewNoteButtonClick = this.handleNewNoteButtonClick.bind(this) } componentDidMount () { - eventEmitter.on('top:new-note', this.newNoteHandler) + eventEmitter.on('top:new-note', this.handleNewNoteButtonClick) } componentWillUnmount () { - eventEmitter.off('top:new-note', this.newNoteHandler) + eventEmitter.off('top:new-note', this.handleNewNoteButtonClick) } handleNewNoteButtonClick (e) { - const { location, params, dispatch, config } = this.props + const { location, dispatch, match: { params }, config } = this.props const { storage, folder } = this.resolveTargetFolder() - if (config.ui.defaultNote === 'MARKDOWN_NOTE') { createMarkdownNote(storage.key, folder.key, dispatch, location, params, config) } else if (config.ui.defaultNote === 'SNIPPET_NOTE') { @@ -55,9 +52,8 @@ class NewNoteButton extends React.Component { } resolveTargetFolder () { - const { data, params } = this.props + const { data, match: { params } } = this.props let storage = data.storageMap.get(params.storageKey) - // Find first storage if (storage == null) { for (const kv of data.storageMap) { @@ -93,7 +89,7 @@ class NewNoteButton extends React.Component { >
diff --git a/browser/main/index.js b/browser/main/index.js index 6e8bdcc5..b3a909e5 100644 --- a/browser/main/index.js +++ b/browser/main/index.js @@ -1,11 +1,13 @@ import { Provider } from 'react-redux' import Main from './Main' -import store from './store' -import React from 'react' +import { store, history } from './store' +import React, { Fragment } from 'react' import ReactDOM from 'react-dom' require('!!style!css!stylus?sourceMap!./global.styl') -import { Router, Route, IndexRoute, IndexRedirect, hashHistory } from 'react-router' -import { syncHistoryWithStore } from 'react-router-redux' +import { Route, Switch, Redirect } from 'react-router-dom' +import { ConnectedRouter } from 'connected-react-router' +import DevTools from './DevTools' + require('./lib/ipcClient') require('../lib/customMeta') import i18n from 'browser/lib/i18n' @@ -77,7 +79,6 @@ document.addEventListener('click', function (e) { }) const el = document.getElementById('content') -const history = syncHistoryWithStore(hashHistory, store) function notify (...args) { return new window.Notification(...args) @@ -98,29 +99,24 @@ function updateApp () { ReactDOM.render(( - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + {/* storages */} + + + + + + + ), el, function () { const loadingCover = document.getElementById('loadingCover') diff --git a/browser/main/lib/ConfigManager.js b/browser/main/lib/ConfigManager.js index bdb55895..e5a0f0c3 100644 --- a/browser/main/lib/ConfigManager.js +++ b/browser/main/lib/ConfigManager.js @@ -11,6 +11,10 @@ const consts = require('browser/lib/consts') let isInitialized = false +const DEFAULT_MARKDOWN_LINT_CONFIG = `{ + "default": true +}` + export const DEFAULT_CONFIG = { zoom: 1, isSideNavFolded: false, @@ -47,7 +51,7 @@ export const DEFAULT_CONFIG = { enableRulers: false, rulers: [80, 120], displayLineNumbers: true, - matchingPairs: '()[]{}\'\'""$$**``', + matchingPairs: '()[]{}\'\'""$$**``~~__', matchingTriples: '```"""\'\'\'', explodingPairs: '[]{}``$$', switchPreview: 'BLUR', // 'BLUR', 'DBL_CLICK', 'RIGHTCLICK' @@ -60,7 +64,9 @@ export const DEFAULT_CONFIG = { enableFrontMatterTitle: true, frontMatterTitleField: 'title', spellcheck: false, - enableSmartPaste: false + enableSmartPaste: false, + enableMarkdownLint: false, + customMarkdownLintConfig: DEFAULT_MARKDOWN_LINT_CONFIG }, preview: { fontSize: '14', @@ -133,16 +139,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' } } @@ -178,16 +180,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/lib/dataApi/exportFolder.js b/browser/main/lib/dataApi/exportFolder.js index 771f77dc..8f15b147 100644 --- a/browser/main/lib/dataApi/exportFolder.js +++ b/browser/main/lib/dataApi/exportFolder.js @@ -43,19 +43,18 @@ function exportFolder (storageKey, folderKey, fileType, exportDir) { .then(function exportNotes (data) { const { storage, notes } = data - notes + return Promise.all(notes .filter(note => note.folder === folderKey && note.isTrashed === false && note.type === 'MARKDOWN_NOTE') - .forEach(note => { + .map(note => { const notePath = path.join(exportDir, `${filenamify(note.title, {replacement: '_'})}.${fileType}`) - exportNote(note.key, storage.path, note.content, notePath, null) + return exportNote(note.key, storage.path, note.content, notePath, null) }) - - return { + ).then(() => ({ storage, folderKey, fileType, exportDir - } + })) }) } diff --git a/browser/main/lib/dataApi/exportNote.js b/browser/main/lib/dataApi/exportNote.js index b358e548..75c451c1 100755 --- a/browser/main/lib/dataApi/exportNote.js +++ b/browser/main/lib/dataApi/exportNote.js @@ -43,14 +43,17 @@ function exportNote (nodeKey, storageKey, noteContent, targetPath, outputFormatt ) if (outputFormatter) { - exportedData = outputFormatter(exportedData, exportTasks) + exportedData = outputFormatter(exportedData, exportTasks, path.dirname(targetPath)) + } else { + exportedData = Promise.resolve(exportedData) } const tasks = prepareTasks(exportTasks, storagePath, path.dirname(targetPath)) return Promise.all(tasks.map((task) => copyFile(task.src, task.dst))) - .then(() => { - return saveToFile(exportedData, targetPath) + .then(() => exportedData) + .then(data => { + return saveToFile(data, targetPath) }).catch((err) => { rollbackExport(tasks) throw err diff --git a/browser/main/lib/modal.js b/browser/main/lib/modal.js index 7a7a9c8c..955cb5c8 100644 --- a/browser/main/lib/modal.js +++ b/browser/main/lib/modal.js @@ -1,7 +1,7 @@ import React from 'react' import { Provider } from 'react-redux' import ReactDOM from 'react-dom' -import store from '../store' +import { store } from '../store' class ModalBase extends React.Component { constructor (props) { diff --git a/browser/main/modals/CreateFolderModal.js b/browser/main/modals/CreateFolderModal.js index b061b0f3..b48d6e42 100644 --- a/browser/main/modals/CreateFolderModal.js +++ b/browser/main/modals/CreateFolderModal.js @@ -3,7 +3,7 @@ import React from 'react' import CSSModules from 'browser/lib/CSSModules' import styles from './CreateFolderModal.styl' import dataApi from 'browser/main/lib/dataApi' -import store from 'browser/main/store' +import { store } from 'browser/main/store' import consts from 'browser/lib/consts' import ModalEscButton from 'browser/components/ModalEscButton' import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig' diff --git a/browser/main/modals/NewNoteModal.js b/browser/main/modals/NewNoteModal.js index a190602c..a17a36cd 100644 --- a/browser/main/modals/NewNoteModal.js +++ b/browser/main/modals/NewNoteModal.js @@ -4,11 +4,12 @@ import styles from './NewNoteModal.styl' import ModalEscButton from 'browser/components/ModalEscButton' import i18n from 'browser/lib/i18n' import { createMarkdownNote, createSnippetNote } from 'browser/lib/newNote' +import queryString from 'query-string' class NewNoteModal extends React.Component { constructor (props) { super(props) - + this.lock = false this.state = {} } @@ -21,10 +22,14 @@ 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) - }) + const { storage, folder, dispatch, location, config } = this.props + const params = location.search !== '' && queryString.parse(location.search) + if (!this.lock) { + this.lock = true + createMarkdownNote(storage, folder, dispatch, location, params, config).then(() => { + setTimeout(this.props.close, 200) + }) + } } handleMarkdownNoteButtonKeyDown (e) { @@ -35,10 +40,14 @@ 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) - }) + const { storage, folder, dispatch, location, config } = this.props + const params = location.search !== '' && queryString.parse(location.search) + if (!this.lock) { + this.lock = true + createSnippetNote(storage, folder, dispatch, location, params, config).then(() => { + setTimeout(this.props.close, 200) + }) + } } handleSnippetNoteButtonKeyDown (e) { diff --git a/browser/main/modals/PreferencesModal/Blog.js b/browser/main/modals/PreferencesModal/Blog.js index 2c93fb29..4d59bea1 100644 --- a/browser/main/modals/PreferencesModal/Blog.js +++ b/browser/main/modals/PreferencesModal/Blog.js @@ -2,7 +2,7 @@ import React from 'react' import CSSModules from 'browser/lib/CSSModules' import styles from './ConfigTab.styl' import ConfigManager from 'browser/main/lib/ConfigManager' -import store from 'browser/main/store' +import { store } from 'browser/main/store' import PropTypes from 'prop-types' import _ from 'lodash' import i18n from 'browser/lib/i18n' diff --git a/browser/main/modals/PreferencesModal/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/FolderItem.js b/browser/main/modals/PreferencesModal/FolderItem.js index dc9082b9..e6bd1e37 100644 --- a/browser/main/modals/PreferencesModal/FolderItem.js +++ b/browser/main/modals/PreferencesModal/FolderItem.js @@ -4,7 +4,7 @@ import CSSModules from 'browser/lib/CSSModules' import ReactDOM from 'react-dom' import styles from './FolderItem.styl' import dataApi from 'browser/main/lib/dataApi' -import store from 'browser/main/store' +import { store } from 'browser/main/store' import { SketchPicker } from 'react-color' import { SortableElement, SortableHandle } from 'react-sortable-hoc' import i18n from 'browser/lib/i18n' diff --git a/browser/main/modals/PreferencesModal/FolderList.js b/browser/main/modals/PreferencesModal/FolderList.js index e7cc6f94..02f5cee9 100644 --- a/browser/main/modals/PreferencesModal/FolderList.js +++ b/browser/main/modals/PreferencesModal/FolderList.js @@ -3,7 +3,7 @@ import React from 'react' import CSSModules from 'browser/lib/CSSModules' import dataApi from 'browser/main/lib/dataApi' import styles from './FolderList.styl' -import store from 'browser/main/store' +import { store } from 'browser/main/store' import FolderItem from './FolderItem' import { SortableContainer } from 'react-sortable-hoc' import i18n from 'browser/lib/i18n' diff --git a/browser/main/modals/PreferencesModal/HotkeyTab.js b/browser/main/modals/PreferencesModal/HotkeyTab.js index 218a68f6..713f6a65 100644 --- a/browser/main/modals/PreferencesModal/HotkeyTab.js +++ b/browser/main/modals/PreferencesModal/HotkeyTab.js @@ -3,7 +3,7 @@ import React from 'react' import CSSModules from 'browser/lib/CSSModules' import styles from './ConfigTab.styl' import ConfigManager from 'browser/main/lib/ConfigManager' -import store from 'browser/main/store' +import { store } from 'browser/main/store' import _ from 'lodash' import i18n from 'browser/lib/i18n' diff --git a/browser/main/modals/PreferencesModal/InfoTab.js b/browser/main/modals/PreferencesModal/InfoTab.js index dafabb02..71e99da9 100644 --- a/browser/main/modals/PreferencesModal/InfoTab.js +++ b/browser/main/modals/PreferencesModal/InfoTab.js @@ -2,7 +2,7 @@ import React from 'react' import CSSModules from 'browser/lib/CSSModules' import styles from './InfoTab.styl' import ConfigManager from 'browser/main/lib/ConfigManager' -import store from 'browser/main/store' +import { store } from 'browser/main/store' import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig' import _ from 'lodash' import i18n from 'browser/lib/i18n' @@ -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/SnippetEditor.js b/browser/main/modals/PreferencesModal/SnippetEditor.js index 071f265f..e95afdcf 100644 --- a/browser/main/modals/PreferencesModal/SnippetEditor.js +++ b/browser/main/modals/PreferencesModal/SnippetEditor.js @@ -4,6 +4,7 @@ import _ from 'lodash' import styles from './SnippetTab.styl' import CSSModules from 'browser/lib/CSSModules' import dataApi from 'browser/main/lib/dataApi' +import snippetManager from '../../../lib/SnippetManager' const defaultEditorFontFamily = ['Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', 'monospace'] const buildCMRulers = (rulers, enableRulers) => @@ -64,7 +65,9 @@ class SnippetEditor extends React.Component { } saveSnippet () { - dataApi.updateSnippet(this.snippet).catch((err) => { throw err }) + dataApi.updateSnippet(this.snippet) + .then(snippets => snippetManager.assignSnippets(snippets)) + .catch((err) => { throw err }) } render () { 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')}
    { this.setState({UiAlert: { type: 'success', @@ -101,7 +103,9 @@ class UiTab extends React.Component { matchingTriples: this.refs.matchingTriples.value, explodingPairs: this.refs.explodingPairs.value, spellcheck: this.refs.spellcheck.checked, - enableSmartPaste: this.refs.enableSmartPaste.checked + enableSmartPaste: this.refs.enableSmartPaste.checked, + enableMarkdownLint: this.refs.enableMarkdownLint.checked, + customMarkdownLintConfig: this.customMarkdownLintConfigCM.getCodeMirror().getValue() }, preview: { fontSize: this.refs.previewFontSize.value, @@ -128,8 +132,13 @@ class UiTab extends React.Component { const newCodemirrorTheme = this.refs.editorTheme.value if (newCodemirrorTheme !== codemirrorTheme) { - checkHighLight.setAttribute('href', `../node_modules/codemirror/theme/${newCodemirrorTheme.split(' ')[0]}.css`) + const theme = consts.THEMES.find(theme => 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 +364,7 @@ class UiTab extends React.Component { > { themes.map((theme) => { - return () + return () }) } @@ -492,7 +501,7 @@ class UiTab extends React.Component { ref='editorSnippetDefaultLanguage' onChange={(e) => this.handleUIChange(e)} > - + { _.sortBy(CodeMirror.modeInfo.map(mode => mode.name)).map(name => ()) } @@ -632,6 +641,34 @@ class UiTab extends React.Component { />
    +
    +
    + {i18n.__('Custom MarkdownLint Rules')} +
    +
    + this.handleUIChange(e)} + checked={this.state.config.editor.enableMarkdownLint} + ref='enableMarkdownLint' + type='checkbox' + />  + {i18n.__('Enable MarkdownLint')} +
    + this.handleUIChange(e)} + ref={e => (this.customMarkdownLintConfigCM = e)} + value={config.editor.customMarkdownLintConfig} + options={{ + lineNumbers: true, + mode: 'application/json', + theme: codemirrorTheme, + lint: true, + gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter', 'CodeMirror-lint-markers'] + }} /> +
    +
    +
    {i18n.__('Preview')}
    @@ -670,7 +707,7 @@ class UiTab extends React.Component { > { themes.map((theme) => { - return () + return () }) } @@ -846,6 +883,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/modals/RenameFolderModal.js b/browser/main/modals/RenameFolderModal.js index edbcee67..9fdd70c8 100644 --- a/browser/main/modals/RenameFolderModal.js +++ b/browser/main/modals/RenameFolderModal.js @@ -3,7 +3,7 @@ import React from 'react' import CSSModules from 'browser/lib/CSSModules' import styles from './RenameFolderModal.styl' import dataApi from 'browser/main/lib/dataApi' -import store from 'browser/main/store' +import { store } from 'browser/main/store' import ModalEscButton from 'browser/components/ModalEscButton' import i18n from 'browser/lib/i18n' diff --git a/browser/main/store.js b/browser/main/store.js index 11ff2f3f..c708c3ad 100644 --- a/browser/main/store.js +++ b/browser/main/store.js @@ -1,8 +1,10 @@ -import { combineReducers, createStore } from 'redux' -import { routerReducer } from 'react-router-redux' +import { combineReducers, createStore, compose, applyMiddleware } from 'redux' +import { connectRouter, routerMiddleware } from 'connected-react-router' +import { createHashHistory as createHistory } from 'history' import ConfigManager from 'browser/main/lib/ConfigManager' import { Map, Set } from 'browser/lib/Mutable' import _ from 'lodash' +import DevTools from './DevTools' function defaultDataMap () { return { @@ -44,7 +46,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': @@ -463,13 +467,16 @@ function getOrInitItem (target, key) { return results } +const history = createHistory() + const reducer = combineReducers({ data, config, status, - routing: routerReducer + router: connectRouter(history) }) -const store = createStore(reducer) +const store = createStore(reducer, undefined, compose( + applyMiddleware(routerMiddleware(history)), DevTools.instrument())) -export default store +export { store, history } diff --git a/docs/code_style.md b/docs/code_style.md index d8f458d7..c0416216 100644 --- a/docs/code_style.md +++ b/docs/code_style.md @@ -79,4 +79,11 @@ class MyComponent extends React.Component { // code goes here... } } -``` \ No newline at end of file +``` + +## React Hooks +Existing code will be kept class-based and will only be changed to functional components with hooks if it improves readability or makes things more reusable. + +For new components it's OK to use hooks with functional components but don't mix hooks & class-based components within a feature - just for code style / readability reasons. + +Read more about hooks in the [React hooks introduction](https://reactjs.org/docs/hooks-intro.html). 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-menu.js b/lib/main-menu.js index dcd85217..f20f1e90 100644 --- a/lib/main-menu.js +++ b/lib/main-menu.js @@ -141,6 +141,13 @@ const file = { mainWindow.webContents.send('list:isMarkdownNote', 'export-html') mainWindow.webContents.send('export:save-html') } + }, + { + label: 'PDF (.pdf)', + click () { + mainWindow.webContents.send('list:isMarkdownNote', 'export-pdf') + mainWindow.webContents.send('export:save-pdf') + } } ] }, diff --git a/lib/main-window.js b/lib/main-window.js index 418fd442..e650cb92 100644 --- a/lib/main-window.js +++ b/lib/main-window.js @@ -6,6 +6,33 @@ const Config = require('electron-config') const config = new Config() const _ = require('lodash') +// set up some chrome extensions +if (process.env.NODE_ENV === 'development') { + const { + default: installExtension, + REACT_DEVELOPER_TOOLS, + REACT_PERF + } = require('electron-devtools-installer') + + require('electron-debug')({ showDevTools: false }) + + const ChromeLens = { + // ID of the extension (https://chrome.google.com/webstore/detail/chromelens/idikgljglpfilbhaboonnpnnincjhjkd) + id: 'idikgljglpfilbhaboonnpnnincjhjkd', + electron: '>=1.2.1' + } + + const extensions = [REACT_DEVELOPER_TOOLS, REACT_PERF, ChromeLens] + + for (const extension of extensions) { + try { + installExtension(extension) + } catch (e) { + console.error(`[ELECTRON] Extension installation failed`, e) + } + } +} + const windowSize = config.get('windowsize') || { x: null, y: null, @@ -27,8 +54,7 @@ const mainWindow = new BrowserWindow({ }, icon: path.resolve(__dirname, '../resources/app.png') }) - -const url = path.resolve(__dirname, './main.html') +const url = path.resolve(__dirname, process.env.NODE_ENV === 'production' ? './main.production.html' : './main.development.html') mainWindow.loadURL('file://' + url) mainWindow.setMenuBarVisibility(false) diff --git a/lib/main.development.html b/lib/main.development.html new file mode 100644 index 00000000..38e2cea9 --- /dev/null +++ b/lib/main.development.html @@ -0,0 +1,166 @@ + + + + + + + + + + + + + + + + Boostnote + + + + + +
    + +
    + +
    +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/main.html b/lib/main.production.html similarity index 95% rename from lib/main.html rename to lib/main.production.html index a1ea3610..ffd9eec3 100644 --- a/lib/main.html +++ b/lib/main.production.html @@ -131,9 +131,9 @@ window._ = require('lodash') - - - + + +