1
0
mirror of https://github.com/BoostIo/Boostnote synced 2025-12-14 10:16:26 +00:00

Merge branch 'master' into fix-scroll

This commit is contained in:
Baptiste Augrain
2018-12-24 10:33:47 +01:00
45 changed files with 1154 additions and 511 deletions

View File

@@ -1,4 +1,4 @@
# EditorConfig is awesome: http://EditorConfig.org # EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file # top-most EditorConfig file
root = true root = true

2
.vscode/launch.json vendored
View File

@@ -17,7 +17,7 @@
"${workspaceFolder}/index.js" "${workspaceFolder}/index.js"
], ],
"windows": { "windows": {
"runtimeExecutable": "${workspaceFolder}/node_modeules/.bin/electron.cmd" "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd"
} }
}, },
{ {

View File

@@ -1,72 +0,0 @@
<h1 align="center">Sponsors &amp; Backers</h1>
Boostnote is an open source project. It's an independent project with its ongoing development made possible entirely thanks to the support by these awesome backers. If you'd like to join them, please consider:
- [Become a backer or sponsor on Open Collective.](https://opencollective.com/boostnoteio)
---
## Backers via OpenCollective
### [Gold Sponsors / $1,000 per month](https://opencollective.com/boostnoteio/order/2259)
- Get your logo on our Readme.md on GitHub and the frontpage of https://boostnote.io/.
### [Silver Sponsors / $250 per month](https://opencollective.com/boostnoteio/order/2257)
- Get your logo on our Readme.md on GitHub and the frontpage of https://boostnote.io/.
### [Bronze Sponsors / $50 per month](https://opencollective.com/boostnoteio/order/2258)
- Get your name and Url (or E-mail) on Readme.md on GitHub.
### [Backers3 / $10 per month](https://opencollective.com/boostnoteio/order/2176)
- [Ralph03](https://opencollective.com/ralph03)
- [Nikolas Dan](https://opencollective.com/nikolas-dan)
### [Backers2 / $5 per month](https://opencollective.com/boostnoteio/order/2175)
- [Yeojong Kim](https://twitter.com/yeojoy)
- [Scotia Draven](https://opencollective.com/scotia-draven)
- [A. J. Vargas](https://opencollective.com/aj-vargas)
### [Backers1](https://opencollective.com/boostnoteio/order/2563) and One-time sponsors
- Ryosuke Tamura - $30
- tatoosh11 - $10
- Alexander Borovkov - $10
- spoonhoop - $5
- Drew Williams - $2
- Andy Shaw - $2
- mysafesky -$2
---
## Backers via Bountysource
https://salt.bountysource.com/teams/boostnote
- Kuzz - $65
- Intense Raiden - $45
- ravy22 - $25
- trentpolack - $20
- hikariru - $10
- kolchan11 - $10
- RonWalker22 - $10
- hocchuc - $5
- Adam - $5
- Steve - $5
- evmin - $5

View File

@@ -13,7 +13,7 @@ import crypto from 'crypto'
import consts from 'browser/lib/consts' import consts from 'browser/lib/consts'
import styles from '../components/CodeEditor.styl' import styles from '../components/CodeEditor.styl'
import fs from 'fs' import fs from 'fs'
const { ipcRenderer, remote } = require('electron') const { ipcRenderer, remote, clipboard } = require('electron')
import normalizeEditorFontFamily from 'browser/lib/normalizeEditorFontFamily' import normalizeEditorFontFamily from 'browser/lib/normalizeEditorFontFamily'
const spellcheck = require('browser/lib/spellcheck') const spellcheck = require('browser/lib/spellcheck')
const buildEditorContextMenu = require('browser/lib/contextMenuBuilder') const buildEditorContextMenu = require('browser/lib/contextMenuBuilder')
@@ -25,6 +25,10 @@ CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js'
const buildCMRulers = (rulers, enableRulers) => const buildCMRulers = (rulers, enableRulers) =>
(enableRulers ? rulers.map(ruler => ({ column: ruler })) : []) (enableRulers ? rulers.map(ruler => ({ column: ruler })) : [])
function translateHotkey (hotkey) {
return hotkey.replace(/\s*\+\s*/g, '-').replace(/Command/g, 'Cmd').replace(/Control/g, 'Ctrl')
}
export default class CodeEditor extends React.Component { export default class CodeEditor extends React.Component {
constructor (props) { constructor (props) {
super(props) super(props)
@@ -34,6 +38,7 @@ export default class CodeEditor extends React.Component {
trailing: true trailing: true
}) })
this.changeHandler = (editor, changeObject) => this.handleChange(editor, changeObject) this.changeHandler = (editor, changeObject) => this.handleChange(editor, changeObject)
this.highlightHandler = (editor, changeObject) => this.handleHighlight(editor, changeObject)
this.focusHandler = () => { this.focusHandler = () => {
ipcRenderer.send('editor:focused', true) ipcRenderer.send('editor:focused', true)
} }
@@ -56,7 +61,11 @@ export default class CodeEditor extends React.Component {
noteKey noteKey
) )
} }
this.pasteHandler = (editor, e) => this.handlePaste(editor, e) this.pasteHandler = (editor, e) => {
e.preventDefault()
this.handlePaste(editor, false)
}
this.loadStyleHandler = e => { this.loadStyleHandler = e => {
this.editor.refresh() this.editor.refresh()
} }
@@ -65,12 +74,16 @@ export default class CodeEditor extends React.Component {
this.scrollToLineHandeler = this.scrollToLine.bind(this) this.scrollToLineHandeler = this.scrollToLine.bind(this)
this.formatTable = () => this.handleFormatTable() this.formatTable = () => this.handleFormatTable()
if (props.switchPreview !== 'RIGHTCLICK') {
this.contextMenuHandler = function (editor, event) { this.contextMenuHandler = function (editor, event) {
const menu = buildEditorContextMenu(editor, event) const menu = buildEditorContextMenu(editor, event)
if (menu != null) { if (menu != null) {
setTimeout(() => menu.popup(remote.getCurrentWindow()), 30) setTimeout(() => menu.popup(remote.getCurrentWindow()), 30)
} }
} }
}
this.editorActivityHandler = () => this.handleEditorActivity() this.editorActivityHandler = () => this.handleEditorActivity()
this.turndownService = new TurndownService() this.turndownService = new TurndownService()
@@ -124,42 +137,9 @@ export default class CodeEditor extends React.Component {
} }
} }
updateTableEditorState () { updateDefaultKeyMap () {
const active = this.tableEditor.cursorIsInTable(this.tableEditorOptions) const { hotkey } = this.props
if (active) {
if (this.extraKeysMode !== 'editor') {
this.extraKeysMode = 'editor'
this.editor.setOption('extraKeys', this.editorKeyMap)
}
} else {
if (this.extraKeysMode !== 'default') {
this.extraKeysMode = 'default'
this.editor.setOption('extraKeys', this.defaultKeyMap)
this.tableEditor.resetSmartCursor()
}
}
}
componentDidMount () {
const { rulers, enableRulers } = this.props
const expandSnippet = this.expandSnippet.bind(this) const expandSnippet = this.expandSnippet.bind(this)
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'
)
}
this.defaultKeyMap = CodeMirror.normalizeKeyMap({ this.defaultKeyMap = CodeMirror.normalizeKeyMap({
Tab: function (cm) { Tab: function (cm) {
@@ -202,6 +182,9 @@ export default class CodeEditor extends React.Component {
} }
} }
}, },
'Cmd-Left': function (cm) {
cm.execCommand('goLineLeft')
},
'Cmd-T': function (cm) { 'Cmd-T': function (cm) {
// Do nothing // Do nothing
}, },
@@ -211,13 +194,56 @@ export default class CodeEditor extends React.Component {
document.execCommand('copy') document.execCommand('copy')
} }
return CodeMirror.Pass return CodeMirror.Pass
},
[translateHotkey(hotkey.pasteSmartly)]: cm => {
this.handlePaste(cm, true)
} }
}) })
}
updateTableEditorState () {
const active = this.tableEditor.cursorIsInTable(this.tableEditorOptions)
if (active) {
if (this.extraKeysMode !== 'editor') {
this.extraKeysMode = 'editor'
this.editor.setOption('extraKeys', this.editorKeyMap)
}
} else {
if (this.extraKeysMode !== 'default') {
this.extraKeysMode = 'default'
this.editor.setOption('extraKeys', this.defaultKeyMap)
this.tableEditor.resetSmartCursor()
}
}
}
componentDidMount () {
const { rulers, enableRulers } = 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'
)
}
this.updateDefaultKeyMap()
this.value = this.props.value this.value = this.props.value
this.editor = CodeMirror(this.refs.root, { this.editor = CodeMirror(this.refs.root, {
rulers: buildCMRulers(rulers, enableRulers), rulers: buildCMRulers(rulers, enableRulers),
value: this.props.value, value: this.props.value,
linesHighlighted: this.props.linesHighlighted,
lineNumbers: this.props.displayLineNumbers, lineNumbers: this.props.displayLineNumbers,
lineWrapping: true, lineWrapping: true,
theme: this.props.theme, theme: this.props.theme,
@@ -244,8 +270,11 @@ export default class CodeEditor extends React.Component {
this.editor.on('focus', this.focusHandler) this.editor.on('focus', this.focusHandler)
this.editor.on('blur', this.blurHandler) this.editor.on('blur', this.blurHandler)
this.editor.on('change', this.changeHandler) this.editor.on('change', this.changeHandler)
this.editor.on('gutterClick', this.highlightHandler)
this.editor.on('paste', this.pasteHandler) this.editor.on('paste', this.pasteHandler)
if (this.props.switchPreview !== 'RIGHTCLICK') {
this.editor.on('contextmenu', this.contextMenuHandler) this.editor.on('contextmenu', this.contextMenuHandler)
}
eventEmitter.on('top:search', this.searchHandler) eventEmitter.on('top:search', this.searchHandler)
eventEmitter.emit('code:init') eventEmitter.emit('code:init')
@@ -320,6 +349,8 @@ export default class CodeEditor extends React.Component {
this.setState({ this.setState({
clientWidth: this.refs.root.clientWidth clientWidth: this.refs.root.clientWidth
}) })
this.initialHighlighting()
} }
expandSnippet (line, cursor, cm, snippets) { expandSnippet (line, cursor, cm, snippets) {
@@ -483,6 +514,14 @@ export default class CodeEditor extends React.Component {
this.editor.setOption('extraKeys', this.defaultKeyMap) this.editor.setOption('extraKeys', this.defaultKeyMap)
} }
if (prevProps.hotkey !== this.props.hotkey) {
this.updateDefaultKeyMap()
if (this.extraKeysMode === 'default') {
this.editor.setOption('extraKeys', this.defaultKeyMap)
}
}
if (this.state.clientWidth !== this.refs.root.clientWidth) { if (this.state.clientWidth !== this.refs.root.clientWidth) {
this.setState({ this.setState({
clientWidth: this.refs.root.clientWidth clientWidth: this.refs.root.clientWidth
@@ -516,12 +555,96 @@ export default class CodeEditor extends React.Component {
handleChange (editor, changeObject) { handleChange (editor, changeObject) {
spellcheck.handleChange(editor, changeObject) spellcheck.handleChange(editor, changeObject)
this.updateHighlight(editor, changeObject)
this.value = editor.getValue() this.value = editor.getValue()
if (this.props.onChange) { if (this.props.onChange) {
this.props.onChange(editor) this.props.onChange(editor)
} }
} }
incrementLines (start, linesAdded, linesRemoved, editor) {
let highlightedLines = editor.options.linesHighlighted
const totalHighlightedLines = highlightedLines.length
let offset = linesAdded - linesRemoved
// Store new items to be added as we're changing the lines
let newLines = []
let i = totalHighlightedLines
while (i--) {
const lineNumber = highlightedLines[i]
// Interval that will need to be updated
// Between start and (start + offset) remove highlight
if (lineNumber >= start) {
highlightedLines.splice(highlightedLines.indexOf(lineNumber), 1)
// Lines that need to be relocated
if (lineNumber >= (start + linesRemoved)) {
newLines.push(lineNumber + offset)
}
}
}
// Adding relocated lines
highlightedLines.push(...newLines)
if (this.props.onChange) {
this.props.onChange(editor)
}
}
handleHighlight (editor, changeObject) {
const lines = editor.options.linesHighlighted
if (!lines.includes(changeObject)) {
lines.push(changeObject)
editor.addLineClass(changeObject, 'text', 'CodeMirror-activeline-background')
} else {
lines.splice(lines.indexOf(changeObject), 1)
editor.removeLineClass(changeObject, 'text', 'CodeMirror-activeline-background')
}
if (this.props.onChange) {
this.props.onChange(editor)
}
}
updateHighlight (editor, changeObject) {
const linesAdded = changeObject.text.length - 1
const linesRemoved = changeObject.removed.length - 1
// If no lines added or removed return
if (linesAdded === 0 && linesRemoved === 0) {
return
}
let start = changeObject.from.line
switch (changeObject.origin) {
case '+insert", "undo':
start += 1
break
case 'paste':
case '+delete':
case '+input':
if (changeObject.to.ch !== 0 || changeObject.from.ch !== 0) {
start += 1
}
break
default:
return
}
this.incrementLines(start, linesAdded, linesRemoved, editor)
}
moveCursorTo (row, col) {} moveCursorTo (row, col) {}
scrollToLine (event, num) { scrollToLine (event, num) {
@@ -546,6 +669,7 @@ export default class CodeEditor extends React.Component {
this.value = this.props.value this.value = this.props.value
this.editor.setValue(this.props.value) this.editor.setValue(this.props.value)
this.editor.clearHistory() this.editor.clearHistory()
this.restartHighlighting()
this.editor.on('change', this.changeHandler) this.editor.on('change', this.changeHandler)
this.editor.refresh() this.editor.refresh()
} }
@@ -571,15 +695,14 @@ export default class CodeEditor extends React.Component {
this.editor.replaceSelection(imageMd) this.editor.replaceSelection(imageMd)
} }
handlePaste (editor, e) { handlePaste (editor, forceSmartPaste) {
const clipboardData = e.clipboardData const { storageKey, noteKey, fetchUrlTitle, enableSmartPaste } = this.props
const { storageKey, noteKey } = this.props
const dataTransferItem = clipboardData.items[0]
const pastedTxt = clipboardData.getData('text')
const isURL = str => { const isURL = str => {
const matcher = /^(?:\w+:)?\/\/([^\s\.]+\.\S{2}|localhost[\:?\d]*)\S*$/ const matcher = /^(?:\w+:)?\/\/([^\s\.]+\.\S{2}|localhost[\:?\d]*)\S*$/
return matcher.test(str) return matcher.test(str)
} }
const isInLinkTag = editor => { const isInLinkTag = editor => {
const startCursor = editor.getCursor('start') const startCursor = editor.getCursor('start')
const prevChar = editor.getRange( const prevChar = editor.getRange(
@@ -594,30 +717,73 @@ export default class CodeEditor extends React.Component {
return prevChar === '](' && nextChar === ')' return prevChar === '](' && nextChar === ')'
} }
const pastedHtml = clipboardData.getData('text/html') const isInFencedCodeBlock = editor => {
if (pastedHtml !== '') { const cursor = editor.getCursor()
this.handlePasteHtml(e, editor, pastedHtml)
} else if (dataTransferItem.type.match('image')) { let token = editor.getTokenAt(cursor)
attachmentManagement.handlePastImageEvent( if (token.state.fencedState) {
return true
}
let line = line = cursor.line - 1
while (line >= 0) {
token = editor.getTokenAt({
ch: 3,
line
})
if (token.start === token.end) {
--line
} else if (token.type === 'comment') {
if (line > 0) {
token = editor.getTokenAt({
ch: 3,
line: line - 1
})
return token.type !== 'comment'
} else {
return true
}
} else {
return false
}
}
return false
}
const pastedTxt = clipboard.readText()
if (isInFencedCodeBlock(editor)) {
this.handlePasteText(editor, pastedTxt)
} else if (fetchUrlTitle && isURL(pastedTxt) && !isInLinkTag(editor)) {
this.handlePasteUrl(editor, pastedTxt)
} else if (enableSmartPaste || forceSmartPaste) {
const image = clipboard.readImage()
if (!image.isEmpty()) {
attachmentManagement.handlePastNativeImage(
this, this,
storageKey, storageKey,
noteKey, noteKey,
dataTransferItem image
) )
} else if ( } else {
this.props.fetchUrlTitle && const pastedHtml = clipboard.readHTML()
isURL(pastedTxt) && if (pastedHtml.length > 0) {
!isInLinkTag(editor) this.handlePasteHtml(editor, pastedHtml)
) { } else {
this.handlePasteUrl(e, editor, pastedTxt) this.handlePasteText(editor, pastedTxt)
} }
if (attachmentManagement.isAttachmentLink(pastedTxt)) { }
} else if (attachmentManagement.isAttachmentLink(pastedTxt)) {
attachmentManagement attachmentManagement
.handleAttachmentLinkPaste(storageKey, noteKey, pastedTxt) .handleAttachmentLinkPaste(storageKey, noteKey, pastedTxt)
.then(modifiedText => { .then(modifiedText => {
this.editor.replaceSelection(modifiedText) this.editor.replaceSelection(modifiedText)
}) })
e.preventDefault() } else {
this.handlePasteText(editor, pastedTxt)
} }
} }
@@ -627,8 +793,7 @@ export default class CodeEditor extends React.Component {
} }
} }
handlePasteUrl (e, editor, pastedTxt) { handlePasteUrl (editor, pastedTxt) {
e.preventDefault()
const taggedUrl = `<${pastedTxt}>` const taggedUrl = `<${pastedTxt}>`
editor.replaceSelection(taggedUrl) editor.replaceSelection(taggedUrl)
@@ -667,12 +832,15 @@ export default class CodeEditor extends React.Component {
}) })
} }
handlePasteHtml (e, editor, pastedHtml) { handlePasteHtml (editor, pastedHtml) {
e.preventDefault()
const markdown = this.turndownService.turndown(pastedHtml) const markdown = this.turndownService.turndown(pastedHtml)
editor.replaceSelection(markdown) editor.replaceSelection(markdown)
} }
handlePasteText (editor, pastedTxt) {
editor.replaceSelection(pastedTxt)
}
mapNormalResponse (response, pastedTxt) { mapNormalResponse (response, pastedTxt) {
return this.decodeResponse(response).then(body => { return this.decodeResponse(response).then(body => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@@ -693,6 +861,29 @@ export default class CodeEditor extends React.Component {
}) })
} }
initialHighlighting () {
if (this.editor.options.linesHighlighted == null) {
return
}
const totalHighlightedLines = this.editor.options.linesHighlighted.length
const totalAvailableLines = this.editor.lineCount()
for (let i = 0; i < totalHighlightedLines; i++) {
const lineNumber = this.editor.options.linesHighlighted[i]
if (lineNumber > totalAvailableLines) {
// make sure that we skip the invalid lines althrough this case should not be happened.
continue
}
this.editor.addLineClass(lineNumber, 'text', 'CodeMirror-activeline-background')
}
}
restartHighlighting () {
this.editor.options.linesHighlighted = this.props.linesHighlighted
this.initialHighlighting()
}
mapImageResponse (response, pastedTxt) { mapImageResponse (response, pastedTxt) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
try { try {

View File

@@ -7,6 +7,7 @@ import MarkdownPreview from 'browser/components/MarkdownPreview'
import eventEmitter from 'browser/main/lib/eventEmitter' import eventEmitter from 'browser/main/lib/eventEmitter'
import { findStorage } from 'browser/lib/findStorage' import { findStorage } from 'browser/lib/findStorage'
import ConfigManager from 'browser/main/lib/ConfigManager' import ConfigManager from 'browser/main/lib/ConfigManager'
import attachmentManagement from 'browser/main/lib/dataApi/attachmentManagement'
class MarkdownEditor extends React.Component { class MarkdownEditor extends React.Component {
constructor (props) { constructor (props) {
@@ -221,6 +222,28 @@ class MarkdownEditor extends React.Component {
this.refs.code.editor.replaceSelection(`${mdElement}${this.refs.code.editor.getSelection()}${mdElement}`) this.refs.code.editor.replaceSelection(`${mdElement}${this.refs.code.editor.getSelection()}${mdElement}`)
} }
handleDropImage (dropEvent) {
dropEvent.preventDefault()
const { storageKey, noteKey } = this.props
this.setState({
status: 'CODE'
}, () => {
this.refs.code.focus()
this.refs.code.editor.execCommand('goDocEnd')
this.refs.code.editor.execCommand('goLineEnd')
this.refs.code.editor.execCommand('newlineAndIndent')
attachmentManagement.handleAttachmentDrop(
this.refs.code,
storageKey,
noteKey,
dropEvent
)
})
}
handleKeyUp (e) { handleKeyUp (e) {
const keyPressed = this.state.keyPressed const keyPressed = this.state.keyPressed
keyPressed.delete(e.keyCode) keyPressed.delete(e.keyCode)
@@ -232,7 +255,7 @@ class MarkdownEditor extends React.Component {
} }
render () { render () {
const {className, value, config, storageKey, noteKey} = this.props const {className, value, config, storageKey, noteKey, linesHighlighted} = this.props
let editorFontSize = parseInt(config.editor.fontSize, 10) let editorFontSize = parseInt(config.editor.fontSize, 10)
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14 if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
@@ -275,9 +298,13 @@ class MarkdownEditor extends React.Component {
noteKey={noteKey} noteKey={noteKey}
fetchUrlTitle={config.editor.fetchUrlTitle} fetchUrlTitle={config.editor.fetchUrlTitle}
enableTableEditor={config.editor.enableTableEditor} enableTableEditor={config.editor.enableTableEditor}
linesHighlighted={linesHighlighted}
onChange={(e) => this.handleChange(e)} onChange={(e) => this.handleChange(e)}
onBlur={(e) => this.handleBlur(e)} onBlur={(e) => this.handleBlur(e)}
spellCheck={config.editor.spellcheck} spellCheck={config.editor.spellcheck}
enableSmartPaste={config.editor.enableSmartPaste}
hotkey={config.hotkey}
switchPreview={config.editor.switchPreview}
/> />
<MarkdownPreview styleName={this.state.status === 'PREVIEW' <MarkdownPreview styleName={this.state.status === 'PREVIEW'
? 'preview' ? 'preview'
@@ -311,6 +338,7 @@ class MarkdownEditor extends React.Component {
customCSS={config.preview.customCSS} customCSS={config.preview.customCSS}
allowCustomCSS={config.preview.allowCustomCSS} allowCustomCSS={config.preview.allowCustomCSS}
lineThroughCheckbox={config.preview.lineThroughCheckbox} lineThroughCheckbox={config.preview.lineThroughCheckbox}
onDrop={(e) => this.handleDropImage(e)}
/> />
</div> </div>
) )

View File

@@ -21,6 +21,8 @@ import yaml from 'js-yaml'
import context from 'browser/lib/context' import context from 'browser/lib/context'
import i18n from 'browser/lib/i18n' import i18n from 'browser/lib/i18n'
import fs from 'fs' import fs from 'fs'
import { render } from 'react-dom'
import Carousel from 'react-image-carousel'
import ConfigManager from '../main/lib/ConfigManager' import ConfigManager from '../main/lib/ConfigManager'
const { remote, shell } = require('electron') const { remote, shell } = require('electron')
@@ -40,7 +42,8 @@ const appPath = fileUrl(
) )
const CSS_FILES = [ const CSS_FILES = [
`${appPath}/node_modules/katex/dist/katex.min.css`, `${appPath}/node_modules/katex/dist/katex.min.css`,
`${appPath}/node_modules/codemirror/lib/codemirror.css` `${appPath}/node_modules/codemirror/lib/codemirror.css`,
`${appPath}/node_modules/react-image-carousel/lib/css/main.min.css`
] ]
function buildStyle ( function buildStyle (
@@ -207,7 +210,7 @@ export default class MarkdownPreview extends React.Component {
this.saveAsHtmlHandler = () => this.handleSaveAsHtml() this.saveAsHtmlHandler = () => this.handleSaveAsHtml()
this.printHandler = () => this.handlePrint() this.printHandler = () => this.handlePrint()
this.linkClickHandler = this.handlelinkClick.bind(this) this.linkClickHandler = this.handleLinkClick.bind(this)
this.initMarkdown = this.initMarkdown.bind(this) this.initMarkdown = this.initMarkdown.bind(this)
this.initMarkdown() this.initMarkdown()
} }
@@ -291,26 +294,7 @@ export default class MarkdownPreview extends React.Component {
} }
handleSaveAsMd () { handleSaveAsMd () {
this.exportAsDocument('md', (noteContent, exportTasks) => { this.exportAsDocument('md')
let result = noteContent
if (this.props && this.props.storagePath && this.props.noteKey) {
const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent(
noteContent,
this.props.storagePath
)
attachmentsAbsolutePaths.forEach(attachment => {
exportTasks.push({
src: attachment,
dst: attachmentManagement.DESTINATION_FOLDER
})
})
result = attachmentManagement.removeStorageAndNoteReferences(
noteContent,
this.props.noteKey
)
}
return result
})
} }
handleSaveAsHtml () { handleSaveAsHtml () {
@@ -339,11 +323,6 @@ export default class MarkdownPreview extends React.Component {
) )
let body = this.markdown.render(noteContent) let body = this.markdown.render(noteContent)
const files = [this.GetCodeThemeLink(codeBlockTheme), ...CSS_FILES] const files = [this.GetCodeThemeLink(codeBlockTheme), ...CSS_FILES]
const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent(
noteContent,
this.props.storagePath
)
files.forEach(file => { files.forEach(file => {
if (global.process.platform === 'win32') { if (global.process.platform === 'win32') {
file = file.replace('file:///', '') file = file.replace('file:///', '')
@@ -355,16 +334,6 @@ export default class MarkdownPreview extends React.Component {
dst: 'css' dst: 'css'
}) })
}) })
attachmentsAbsolutePaths.forEach(attachment => {
exportTasks.push({
src: attachment,
dst: attachmentManagement.DESTINATION_FOLDER
})
})
body = attachmentManagement.removeStorageAndNoteReferences(
body,
this.props.noteKey
)
let styles = '' let styles = ''
files.forEach(file => { files.forEach(file => {
@@ -397,8 +366,9 @@ export default class MarkdownPreview extends React.Component {
if (filename) { if (filename) {
const content = this.props.value const content = this.props.value
const storage = this.props.storagePath const storage = this.props.storagePath
const nodeKey = this.props.noteKey
exportNote(storage, content, filename, contentFormatter) exportNote(nodeKey, storage, content, filename, contentFormatter)
.then(res => { .then(res => {
dialog.showMessageBox(remote.getCurrentWindow(), { dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'info', type: 'info',
@@ -443,6 +413,8 @@ export default class MarkdownPreview extends React.Component {
} }
componentDidMount () { componentDidMount () {
const { onDrop } = this.props
this.refs.root.setAttribute('sandbox', 'allow-scripts') this.refs.root.setAttribute('sandbox', 'allow-scripts')
this.refs.root.contentWindow.document.body.addEventListener( this.refs.root.contentWindow.document.body.addEventListener(
'contextmenu', 'contextmenu',
@@ -480,7 +452,7 @@ export default class MarkdownPreview extends React.Component {
) )
this.refs.root.contentWindow.document.addEventListener( this.refs.root.contentWindow.document.addEventListener(
'drop', 'drop',
this.preventImageDroppedHandler onDrop || this.preventImageDroppedHandler
) )
this.refs.root.contentWindow.document.addEventListener( this.refs.root.contentWindow.document.addEventListener(
'dragover', 'dragover',
@@ -497,6 +469,8 @@ export default class MarkdownPreview extends React.Component {
} }
componentWillUnmount () { componentWillUnmount () {
const { onDrop } = this.props
this.refs.root.contentWindow.document.body.removeEventListener( this.refs.root.contentWindow.document.body.removeEventListener(
'contextmenu', 'contextmenu',
this.contextMenuHandler this.contextMenuHandler
@@ -515,7 +489,7 @@ export default class MarkdownPreview extends React.Component {
) )
this.refs.root.contentWindow.document.removeEventListener( this.refs.root.contentWindow.document.removeEventListener(
'drop', 'drop',
this.preventImageDroppedHandler onDrop || this.preventImageDroppedHandler
) )
this.refs.root.contentWindow.document.removeEventListener( this.refs.root.contentWindow.document.removeEventListener(
'dragover', 'dragover',
@@ -800,6 +774,34 @@ export default class MarkdownPreview extends React.Component {
mermaidRender(el, htmlTextHelper.decodeEntities(el.innerHTML), theme) mermaidRender(el, htmlTextHelper.decodeEntities(el.innerHTML), theme)
} }
) )
_.forEach(
this.refs.root.contentWindow.document.querySelectorAll('.gallery'),
el => {
const images = el.innerHTML.split(/\n/g).filter(i => i.length > 0)
el.innerHTML = ''
const height = el.attributes.getNamedItem('data-height')
if (height && height.value !== 'undefined') {
el.style.height = height.value + 'vh'
}
let autoplay = el.attributes.getNamedItem('data-autoplay')
if (autoplay && autoplay.value !== 'undefined') {
autoplay = parseInt(autoplay.value, 10) || 0
} else {
autoplay = 0
}
render(
<Carousel
images={images}
autoplay={autoplay}
/>,
el
)
}
)
} }
focus () { focus () {
@@ -842,7 +844,7 @@ export default class MarkdownPreview extends React.Component {
return new window.Notification(title, options) return new window.Notification(title, options)
} }
handlelinkClick (e) { handleLinkClick (e) {
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()

View File

@@ -64,9 +64,9 @@ class MarkdownSplitEditor extends React.Component {
} }
} }
handleOnChange () { handleOnChange (e) {
this.value = this.refs.code.value this.value = this.refs.code.value
this.props.onChange() this.props.onChange(e)
} }
handleEditorScroll (e) { handleEditorScroll (e) {
@@ -249,7 +249,7 @@ class MarkdownSplitEditor extends React.Component {
} }
render () { render () {
const {config, value, storageKey, noteKey} = this.props const {config, value, storageKey, noteKey, linesHighlighted} = this.props
const storage = findStorage(storageKey) const storage = findStorage(storageKey)
let editorFontSize = parseInt(config.editor.fontSize, 10) let editorFontSize = parseInt(config.editor.fontSize, 10)
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14 if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
@@ -282,10 +282,14 @@ class MarkdownSplitEditor extends React.Component {
enableTableEditor={config.editor.enableTableEditor} enableTableEditor={config.editor.enableTableEditor}
storageKey={storageKey} storageKey={storageKey}
noteKey={noteKey} noteKey={noteKey}
onChange={this.handleOnChange.bind(this)} linesHighlighted={linesHighlighted}
onScroll={this.handleEditorScroll.bind(this)} onChange={(e) => this.handleOnChange(e)}
onCursorActivity={this.handleCursorActivity.bind(this)} onScroll={(e) => this.handleEditorScroll(e)}
onCursorActivity={(e) => this.handleCursorActivity(e)}
spellCheck={config.editor.spellcheck} spellCheck={config.editor.spellcheck}
enableSmartPaste={config.editor.enableSmartPaste}
hotkey={config.hotkey}
switchPreview={config.editor.switchPreview}
/> />
<div styleName='slider' style={{left: this.state.codeEditorWidthInPercent + '%'}} onMouseDown={e => this.handleMouseDown(e)} > <div styleName='slider' style={{left: this.state.codeEditorWidthInPercent + '%'}} onMouseDown={e => this.handleMouseDown(e)} >
<div styleName='slider-hitbox' /> <div styleName='slider-hitbox' />
@@ -309,7 +313,7 @@ class MarkdownSplitEditor extends React.Component {
tabInde='0' tabInde='0'
value={value} value={value}
onCheckboxClick={(e) => this.handleCheckboxClick(e)} onCheckboxClick={(e) => this.handleCheckboxClick(e)}
onScroll={this.handlePreviewScroll.bind(this)} onScroll={(e) => this.handlePreviewScroll(e)}
showCopyNotification={config.ui.showCopyNotification} showCopyNotification={config.ui.showCopyNotification}
storagePath={storage.path} storagePath={storage.path}
noteKey={noteKey} noteKey={noteKey}

View File

@@ -3,19 +3,30 @@
flex 1 flex 1
min-width 70px min-width 70px
overflow hidden overflow hidden
border-left 1px solid $ui-borderColor
border-top 1px solid $ui-borderColor
&:hover &:hover
background-color alpha($ui-button--active-backgroundColor, 20%)
.deleteButton .deleteButton
color $ui-inactive-text-color color: $ui-text-color
&:hover visibility visible
background-color darken($ui-backgroundColor, 15%) transition 0.15s
&:active .button
color white color: $ui-text-color
background-color $ui-active-color transition 0.15s
.root--active .root--active
@extend .root @extend .root
min-width 100px min-width 100px
border-bottom $ui-border background-color alpha($ui-button--active-backgroundColor, 60%)
.deleteButton
visibility visible
color: $ui-text-color
transition 0.15s
.button
font-weight bold
color: $ui-text-color
transition 0.15s
.button .button
width 100% width 100%
@@ -27,8 +38,7 @@
background-color transparent background-color transparent
transition 0.15s transition 0.15s
border-left 4px solid transparent border-left 4px solid transparent
&:hover color $ui-inactive-text-color
background-color $ui-button--hover-backgroundColor
.deleteButton .deleteButton
position absolute position absolute
@@ -42,6 +52,7 @@
color $ui-inactive-text-color color $ui-inactive-text-color
background-color transparent background-color transparent
border-radius 2px border-radius 2px
visibility hidden
.input .input
height 29px height 29px
@@ -50,76 +61,66 @@
width 100% width 100%
outline none outline none
body[data-theme="default"], body[data-theme="white"]
.root--active
&:hover
background-color alpha($ui-button--active-backgroundColor, 60%)
body[data-theme="dark"] body[data-theme="dark"]
.root .root
color $ui-dark-text-color
border-color $ui-dark-borderColor border-color $ui-dark-borderColor
border-top 1px solid $ui-dark-borderColor
&:hover &:hover
background-color $ui-dark-button--hover-backgroundColor background-color alpha($ui-dark-button--active-backgroundColor, 20%)
.deleteButton transition 0.15s
color $ui-dark-inactive-text-color .button
&:hover
background-color darken($ui-dark-button--hover-backgroundColor, 15%)
&:active
color $ui-dark-text-color color $ui-dark-text-color
background-color $ui-dark-button--active-backgroundColor transition 0.15s
.deleteButton
color $ui-dark-text-color
transition 0.15s
.root--active .root--active
color $ui-dark-text-color
border-color $ui-dark-borderColor
&:hover
background-color $ui-dark-button--hover-backgroundColor
.deleteButton
color $ui-dark-inactive-text-color
&:hover
background-color darken($ui-dark-button--hover-backgroundColor, 15%)
&:active
color $ui-dark-text-color
background-color $ui-dark-button--active-backgroundColor background-color $ui-dark-button--active-backgroundColor
border-left 1px solid $ui-dark-borderColor
border-top 1px solid $ui-dark-borderColor
.button
color $ui-dark-text-color
.deleteButton
color $ui-dark-text-color
.button .button
border none border none
color $ui-dark-text-color
background-color transparent background-color transparent
transition color background-color 0.15s transition color background-color 0.15s
border-left 4px solid transparent border-left 4px solid transparent
&:hover
color $ui-dark-text-color
background-color $ui-dark-button--hover-backgroundColor
.input .input
background-color $ui-dark-button--hover-backgroundColor background-color $ui-dark-button--active-backgroundColor
color $ui-dark-text-color color $ui-dark-text-color
transition 0.15s
.deleteButton
color alpha($ui-dark-text-color, 30%)
body[data-theme="solarized-dark"] body[data-theme="solarized-dark"]
.root .root
color $ui-solarized-dark-text-color
border-color $ui-dark-borderColor
&:hover
background-color $ui-solarized-dark-noteDetail-backgroundColor
.deleteButton
color $ui-solarized-dark-text-color
&:hover
background-color darken($ui-solarized-dark-noteDetail-backgroundColor, 15%)
&:active
color $ui-solarized-dark-text-color
background-color $ui-dark-button--active-backgroundColor
.root--active
color $ui-solarized-dark-text-color
border-color $ui-solarized-dark-borderColor border-color $ui-solarized-dark-borderColor
&:hover &:hover
background-color $ui-solarized-dark-noteDetail-backgroundColor background-color $ui-solarized-dark-noteDetail-backgroundColor
transition 0.15s
.deleteButton .deleteButton
color $ui-solarized-dark-text-color color $ui-solarized-dark-button--active-color
&:hover transition 0.15s
background-color darken($ui-solarized-dark-noteDetail-backgroundColor, 15%) .button
&:active color $ui-solarized-dark-button--active-color
color $ui-solarized-dark-text-color transition 0.15s
background-color $ui-dark-button--active-backgroundColor
.root--active
color $ui-solarized-dark-button--active-color
background-color $ui-solarized-dark-button-backgroundColor
border-color $ui-solarized-dark-borderColor
.deleteButton
color $ui-solarized-dark-button--active-color
.button
color $ui-solarized-dark-button--active-color
.button .button
border none border none
@@ -127,101 +128,75 @@ body[data-theme="solarized-dark"]
background-color transparent background-color transparent
transition color background-color 0.15s transition color background-color 0.15s
border-left 4px solid transparent border-left 4px solid transparent
&:hover
color $ui-solarized-dark-text-color
background-color $ui-solarized-dark-noteDetail-backgroundColor
.input .input
background-color $ui-solarized-dark-noteDetail-backgroundColor background-color $ui-solarized-dark-noteDetail-backgroundColor
color $ui-solarized-dark-text-color color $ui-solarized-dark-button--active-color
transition 0.15s
.deleteButton
color alpha($ui-solarized-dark-text-color, 30%)
body[data-theme="monokai"] body[data-theme="monokai"]
.root .root
color $ui-monokai-text-color
border-color $ui-dark-borderColor
&:hover
background-color $ui-monokai-noteDetail-backgroundColor
.deleteButton
color $ui-monokai-text-color
&:hover
background-color darken($ui-monokai-noteDetail-backgroundColor, 15%)
&:active
color $ui-monokai-text-color
background-color $ui-dark-button--active-backgroundColor
.root--active
color $ui-monokai-text-color
border-color $ui-monokai-borderColor border-color $ui-monokai-borderColor
&:hover &:hover
background-color $ui-monokai-noteDetail-backgroundColor background-color $ui-monokai-noteDetail-backgroundColor
transition 0.15s
.deleteButton .deleteButton
color $ui-monokai-text-color color $ui-monokai-text-color
&:hover transition 0.15s
background-color darken($ui-monokai-noteDetail-backgroundColor, 15%) .button
&:active
color $ui-monokai-text-color color $ui-monokai-text-color
background-color $ui-dark-button--active-backgroundColor transition 0.15s
.root--active
color $ui-monokai-active-color
background-color $ui-monokai-button-backgroundColor
border-color $ui-monokai-borderColor
.deleteButton
color $ui-monokai-text-color
.button
color $ui-monokai-active-color
.button .button
border none border none
color $ui-monokai-text-color color $ui-inactive-text-color
background-color transparent background-color transparent
transition color background-color 0.15s transition color background-color 0.15s
border-left 4px solid transparent border-left 4px solid transparent
&:hover
color $ui-monokai-text-color
background-color $ui-monokai-noteDetail-backgroundColor
.input .input
background-color $ui-monokai-noteDetail-backgroundColor background-color $ui-monokai-noteDetail-backgroundColor
color $ui-monokai-text-color color $ui-monokai-text-color
transition 0.15s
.deleteButton
color alpha($ui-monokai-text-color, 30%)
body[data-theme="dracula"] body[data-theme="dracula"]
.root .root
color $ui-dracula-text-color
border-color $ui-dark-borderColor
&:hover
background-color $ui-dracula-noteDetail-backgroundColor
.deleteButton
color $ui-dracula-text-color
&:hover
background-color darken($ui-dracula-noteDetail-backgroundColor, 15%)
&:active
color $ui-dracula-text-color
background-color $ui-dark-button--active-backgroundColor
.root--active
color $ui-dracula-text-color
border-color $ui-dracula-borderColor border-color $ui-dracula-borderColor
&:hover &:hover
background-color $ui-dracula-noteDetail-backgroundColor background-color $ui-dracula-noteDetail-backgroundColor
transition 0.15s
.deleteButton .deleteButton
color $ui-dracula-text-color color $ui-dracula-text-color
&:hover transition 0.15s
background-color darken($ui-dracula-noteDetail-backgroundColor, 15%) .button
&:active
color $ui-dracula-text-color color $ui-dracula-text-color
background-color $ui-dark-button--active-backgroundColor transition 0.15s
.root--active
color $ui-dracula-text-color
background-color $ui-dracula-button-backgroundColor
border-color $ui-dracula-borderColor
.deleteButton
color $ui-dracula-text-color
.button
color $ui-dracula-active-color
.button .button
border none border none
color $ui-dracula-text-color color $ui-inactive-text-color
background-color transparent background-color transparent
transition color background-color 0.15s transition color background-color 0.15s
border-left 4px solid transparent border-left 4px solid transparent
&:hover
color $ui-dracula-text-color
background-color $ui-dracula-noteDetail-backgroundColor
.input .input
background-color $ui-dracula-noteDetail-backgroundColor background-color $ui-dracula-noteDetail-backgroundColor
color $ui-dracula-text-color color $ui-dracula-text-color
.deleteButton
color alpha($ui-dracula-text-color, 30%)

View File

@@ -55,11 +55,12 @@ body
line-height 1.6 line-height 1.6
overflow-x hidden overflow-x hidden
background-color $ui-noteDetail-backgroundColor background-color $ui-noteDetail-backgroundColor
// do not allow display line breaks
.katex-display > .katex
white-space nowrap
// allow inline line breaks
.katex .katex
font 400 1.2em 'KaTeX_Main'
line-height 1.2em
white-space initial white-space initial
text-indent 0
.katex .mfrac>.vlist>span:nth-child(2) .katex .mfrac>.vlist>span:nth-child(2)
top 0 !important top 0 !important
.katex-error .katex-error
@@ -183,6 +184,10 @@ ul
display list-item display list-item
&.taskListItem &.taskListItem
list-style none list-style none
&>input
margin-left -1.6em
&>p
margin-left -1.8em
p p
margin 0 margin 0
&>li>ul, &>li>ol &>li>ul, &>li>ol
@@ -416,6 +421,26 @@ pre.fence
canvas, svg canvas, svg
max-width 100% !important max-width 100% !important
.gallery
width 100%
height 50vh
.carousel
.carousel-main img
min-width auto
max-width 100%
min-height auto
max-height 100%
.carousel-footer::-webkit-scrollbar-corner
background-color transparent
.carousel-main, .carousel-footer
background-color $ui-noteDetail-backgroundColor
.prev, .next
color $ui-text-color
background-color $ui-tag-backgroundColor
themeDarkBackground = darken(#21252B, 10%) themeDarkBackground = darken(#21252B, 10%)
themeDarkText = #f9f9f9 themeDarkText = #f9f9f9
themeDarkBorder = lighten(themeDarkBackground, 20%) themeDarkBorder = lighten(themeDarkBackground, 20%)
@@ -475,6 +500,14 @@ body[data-theme="dark"]
border-color themeDarkBorder border-color themeDarkBorder
background-color themeDarkPreview background-color themeDarkPreview
pre.fence
.gallery
.carousel-main, .carousel-footer
background-color $ui-dark-noteDetail-backgroundColor
.prev, .next
color $ui-dark-text-color
background-color $ui-dark-tag-backgroundColor
themeSolarizedDarkTableOdd = $ui-solarized-dark-noteDetail-backgroundColor themeSolarizedDarkTableOdd = $ui-solarized-dark-noteDetail-backgroundColor
themeSolarizedDarkTableEven = darken($ui-solarized-dark-noteDetail-backgroundColor, 10%) themeSolarizedDarkTableEven = darken($ui-solarized-dark-noteDetail-backgroundColor, 10%)
themeSolarizedDarkTableHead = themeSolarizedDarkTableEven themeSolarizedDarkTableHead = themeSolarizedDarkTableEven
@@ -510,6 +543,14 @@ body[data-theme="solarized-dark"]
border-color themeDarkBorder border-color themeDarkBorder
background-color $ui-solarized-dark-noteDetail-backgroundColor background-color $ui-solarized-dark-noteDetail-backgroundColor
pre.fence
.gallery
.carousel-main, .carousel-footer
background-color $ui-solarized-dark-noteDetail-backgroundColor
.prev, .next
color $ui-solarized-dark-button--active-color
background-color $ui-solarized-dark-button-backgroundColor
themeMonokaiTableOdd = $ui-monokai-noteDetail-backgroundColor themeMonokaiTableOdd = $ui-monokai-noteDetail-backgroundColor
themeMonokaiTableEven = darken($ui-monokai-noteDetail-backgroundColor, 10%) themeMonokaiTableEven = darken($ui-monokai-noteDetail-backgroundColor, 10%)
themeMonokaiTableHead = themeMonokaiTableEven themeMonokaiTableHead = themeMonokaiTableEven
@@ -538,6 +579,7 @@ body[data-theme="monokai"]
border-right solid 1px themeMonokaiTableBorder border-right solid 1px themeMonokaiTableBorder
kbd kbd
background-color themeDarkBackground background-color themeDarkBackground
dl dl
border-color themeDarkBorder border-color themeDarkBorder
background-color themeMonokaiTableHead background-color themeMonokaiTableHead
@@ -547,6 +589,14 @@ body[data-theme="monokai"]
border-color themeDarkBorder border-color themeDarkBorder
background-color $ui-monokai-noteDetail-backgroundColor background-color $ui-monokai-noteDetail-backgroundColor
pre.fence
.gallery
.carousel-main, .carousel-footer
background-color $ui-monokai-noteDetail-backgroundColor
.prev, .next
color $ui-monokai-button--active-color
background-color $ui-monokai-button-backgroundColor
themeDraculaTableOdd = $ui-dracula-noteDetail-backgroundColor themeDraculaTableOdd = $ui-dracula-noteDetail-backgroundColor
themeDraculaTableEven = darken($ui-dracula-noteDetail-backgroundColor, 10%) themeDraculaTableEven = darken($ui-dracula-noteDetail-backgroundColor, 10%)
themeDraculaTableHead = themeDraculaTableEven themeDraculaTableHead = themeDraculaTableEven
@@ -575,6 +625,7 @@ body[data-theme="dracula"]
border-right solid 1px themeDraculaTableBorder border-right solid 1px themeDraculaTableBorder
kbd kbd
background-color themeDarkBackground background-color themeDarkBackground
dl dl
border-color themeDarkBorder border-color themeDarkBorder
background-color themeDraculaTableHead background-color themeDraculaTableHead
@@ -583,3 +634,11 @@ body[data-theme="dracula"]
dd dd
border-color themeDarkBorder border-color themeDarkBorder
background-color $ui-dracula-noteDetail-backgroundColor background-color $ui-dracula-noteDetail-backgroundColor
pre.fence
.gallery
.carousel-main, .carousel-footer
background-color $ui-dracula-noteDetail-backgroundColor
.prev, .next
color $ui-dracula-button--active-color
background-color $ui-dracula-button-backgroundColor

View File

@@ -2,51 +2,27 @@
* @fileoverview Markdown table of contents generator * @fileoverview Markdown table of contents generator
*/ */
import { EOL } from 'os'
import toc from 'markdown-toc' import toc from 'markdown-toc'
import diacritics from 'diacritics-map'
import stripColor from 'strip-color'
import mdlink from 'markdown-link' import mdlink from 'markdown-link'
import slugify from './slugify'
const EOL = require('os').EOL const hasProp = Object.prototype.hasOwnProperty
/** /**
* @caseSensitiveSlugify Custom slugify function * From @enyaxu/markdown-it-anchor
* Same implementation that the original used by markdown-toc (node_modules/markdown-toc/lib/utils.js),
* but keeps original case to properly handle https://github.com/BoostIO/Boostnote/issues/2067
*/ */
function caseSensitiveSlugify (str) { function uniqueSlug (slug, slugs, opts) {
function replaceDiacritics (str) { let uniq = slug
return str.replace(/[À-ž]/g, function (ch) { let i = opts.uniqueSlugStartIndex
return diacritics[ch] || ch while (hasProp.call(slugs, uniq)) uniq = `${slug}-${i++}`
}) slugs[uniq] = true
return uniq
} }
function getTitle (str) { function linkify (token) {
if (/^\[[^\]]+\]\(/.test(str)) { token.content = mdlink(token.content, '#' + token.slug)
var m = /^\[([^\]]+)\]/.exec(str) return token
if (m) return m[1]
}
return str
}
str = getTitle(str)
str = stripColor(str)
// str = str.toLowerCase() //let's be case sensitive
// `.split()` is often (but not always) faster than `.replace()`
str = str.split(' ').join('-')
str = str.split(/\t/).join('--')
str = str.split(/<\/?[^>]+>/).join('')
str = str.split(/[|$&`~=\\\/@+*!?({[\]})<>=.,;:'"^]/).join('')
str = str.split(/[。?!,、;:“”【】()〔〕[]﹃﹄“ ”‘’﹁﹂—…-~《》〈〉「」]/).join('')
str = replaceDiacritics(str)
return str
}
function linkify (tok, text, slug, opts) {
var uniqeID = opts.num === 0 ? '' : '-' + opts.num
tok.content = mdlink(text, '#' + slug + uniqeID)
return tok
} }
const TOC_MARKER_START = '<!-- toc -->' const TOC_MARKER_START = '<!-- toc -->'
@@ -91,8 +67,23 @@ export function generateInEditor (editor) {
* @returns generatedTOC String containing generated TOC * @returns generatedTOC String containing generated TOC
*/ */
export function generate (markdownText) { export function generate (markdownText) {
const generatedToc = toc(markdownText, {slugify: caseSensitiveSlugify, linkify: linkify}) const slugs = {}
return TOC_MARKER_START + EOL + EOL + generatedToc.content + EOL + EOL + TOC_MARKER_END const opts = {
uniqueSlugStartIndex: 1
}
const result = toc(markdownText, {
slugify: title => {
return uniqueSlug(slugify(title), slugs, opts)
},
linkify: false
})
const md = toc.bullets(result.json.map(linkify), {
highest: result.highest
})
return TOC_MARKER_START + EOL + EOL + md + EOL + EOL + TOC_MARKER_END
} }
function wrapTocWithEol (toc, editor) { function wrapTocWithEol (toc, editor) {

View File

@@ -7,7 +7,6 @@ import _ from 'lodash'
import ConfigManager from 'browser/main/lib/ConfigManager' import ConfigManager from 'browser/main/lib/ConfigManager'
import katex from 'katex' import katex from 'katex'
import { lastFindInArray } from './utils' import { lastFindInArray } from './utils'
import anchor from '@enyaxu/markdown-it-anchor'
function createGutter (str, firstLineNumber) { function createGutter (str, firstLineNumber) {
if (Number.isNaN(firstLineNumber)) firstLineNumber = 1 if (Number.isNaN(firstLineNumber)) firstLineNumber = 1
@@ -118,14 +117,8 @@ class Markdown {
this.md.use(require('markdown-it-imsize')) this.md.use(require('markdown-it-imsize'))
this.md.use(require('markdown-it-footnote')) this.md.use(require('markdown-it-footnote'))
this.md.use(require('markdown-it-multimd-table')) this.md.use(require('markdown-it-multimd-table'))
this.md.use(anchor, { this.md.use(require('@enyaxu/markdown-it-anchor'), {
slugify: (title) => { slugify: require('./slugify')
var slug = encodeURI(title.trim()
.replace(/[\]\[\!\"\#\$\%\&\'\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\_\{\|\}\~]/g, '')
.replace(/\s+/g, '-'))
.replace(/\-+$/, '')
return slug
}
}) })
this.md.use(require('markdown-it-kbd')) this.md.use(require('markdown-it-kbd'))
this.md.use(require('markdown-it-admonition'), {types: ['note', 'hint', 'attention', 'caution', 'danger', 'error']}) this.md.use(require('markdown-it-admonition'), {types: ['note', 'hint', 'attention', 'caution', 'danger', 'error']})
@@ -152,6 +145,21 @@ class Markdown {
<div class="flowchart" data-height="${token.parameters.height}">${token.content}</div> <div class="flowchart" data-height="${token.parameters.height}">${token.content}</div>
</pre>` </pre>`
}, },
gallery: token => {
const content = token.content.split('\n').slice(0, -1).map(line => {
const match = /!\[[^\]]*]\(([^\)]*)\)/.exec(line)
if (match) {
return match[1]
} else {
return line
}
}).join('\n')
return `<pre class="fence" data-line="${token.map[0]}">
<span class="filename">${token.fileName}</span>
<div class="gallery" data-autoplay="${token.parameters.autoplay}" data-height="${token.parameters.height}">${content}</div>
</pre>`
},
mermaid: token => { mermaid: token => {
return `<pre class="fence" data-line="${token.map[0]}"> return `<pre class="fence" data-line="${token.map[0]}">
<span class="filename">${token.fileName}</span> <span class="filename">${token.fileName}</span>

View File

@@ -18,7 +18,8 @@ export function createMarkdownNote (storage, folder, dispatch, location, params,
folder: folder, folder: folder,
title: '', title: '',
tags, tags,
content: '' content: '',
linesHighlighted: []
}) })
.then(note => { .then(note => {
const noteHash = note.key const noteHash = note.key
@@ -56,7 +57,8 @@ export function createSnippetNote (storage, folder, dispatch, location, params,
{ {
name: '', name: '',
mode: config.editor.snippetDefaultLanguage || 'text', mode: config.editor.snippetDefaultLanguage || 'text',
content: '' content: '',
linesHighlighted: []
} }
] ]
}) })

17
browser/lib/slugify.js Normal file
View File

@@ -0,0 +1,17 @@
import diacritics from 'diacritics-map'
function replaceDiacritics (str) {
return str.replace(/[À-ž]/g, function (ch) {
return diacritics[ch] || ch
})
}
module.exports = function slugify (title) {
let slug = title.trim()
slug = replaceDiacritics(slug)
slug = slug.replace(/[^\w\s-]/g, '').replace(/\s+/g, '-')
return encodeURI(slug).replace(/\-+$/, '')
}

View File

@@ -70,22 +70,22 @@ class InfoPanel extends React.Component {
<hr /> <hr />
<div id='export-wrap'> <div id='export-wrap'>
<button styleName='export--enable' onClick={(e) => exportAsMd(e)}> <button styleName='export--enable' onClick={(e) => exportAsMd(e, 'export-md')}>
<i className='fa fa-file-code-o' /> <i className='fa fa-file-code-o' />
<p>{i18n.__('.md')}</p> <p>{i18n.__('.md')}</p>
</button> </button>
<button styleName='export--enable' onClick={(e) => exportAsTxt(e)}> <button styleName='export--enable' onClick={(e) => exportAsTxt(e, 'export-txt')}>
<i className='fa fa-file-text-o' /> <i className='fa fa-file-text-o' />
<p>{i18n.__('.txt')}</p> <p>{i18n.__('.txt')}</p>
</button> </button>
<button styleName='export--enable' onClick={(e) => exportAsHtml(e)}> <button styleName='export--enable' onClick={(e) => exportAsHtml(e, 'export-html')}>
<i className='fa fa-html5' /> <i className='fa fa-html5' />
<p>{i18n.__('.html')}</p> <p>{i18n.__('.html')}</p>
</button> </button>
<button styleName='export--enable' onClick={(e) => print(e)}> <button styleName='export--enable' onClick={(e) => print(e, 'print')}>
<i className='fa fa-print' /> <i className='fa fa-print' />
<p>{i18n.__('Print')}</p> <p>{i18n.__('Print')}</p>
</button> </button>

View File

@@ -31,17 +31,17 @@ const InfoPanelTrashed = ({
</div> </div>
<div id='export-wrap'> <div id='export-wrap'>
<button styleName='export--enable' onClick={(e) => exportAsMd(e)}> <button styleName='export--enable' onClick={(e) => exportAsMd(e, 'export-md')}>
<i className='fa fa-file-code-o' /> <i className='fa fa-file-code-o' />
<p>.md</p> <p>.md</p>
</button> </button>
<button styleName='export--enable' onClick={(e) => exportAsTxt(e)}> <button styleName='export--enable' onClick={(e) => exportAsTxt(e, 'export-txt')}>
<i className='fa fa-file-text-o' /> <i className='fa fa-file-text-o' />
<p>.txt</p> <p>.txt</p>
</button> </button>
<button styleName='export--enable' onClick={(e) => exportAsHtml(e)}> <button styleName='export--enable' onClick={(e) => exportAsHtml(e, 'export-html')}>
<i className='fa fa-html5' /> <i className='fa fa-html5' />
<p>.html</p> <p>.html</p>
</button> </button>

View File

@@ -39,12 +39,14 @@ class MarkdownNoteDetail extends React.Component {
isMovingNote: false, isMovingNote: false,
note: Object.assign({ note: Object.assign({
title: '', title: '',
content: '' content: '',
linesHighlighted: []
}, props.note), }, props.note),
isLockButtonShown: false, isLockButtonShown: false,
isLocked: false, isLocked: false,
editorType: props.config.editor.type editorType: props.config.editor.type
} }
this.dispatchTimer = null this.dispatchTimer = null
this.toggleLockButton = this.handleToggleLockButton.bind(this) this.toggleLockButton = this.handleToggleLockButton.bind(this)
@@ -71,7 +73,7 @@ class MarkdownNoteDetail extends React.Component {
if (!this.state.isMovingNote && (isNewNote || hasDeletedTags)) { if (!this.state.isMovingNote && (isNewNote || hasDeletedTags)) {
if (this.saveQueue != null) this.saveNow() if (this.saveQueue != null) this.saveNow()
this.setState({ this.setState({
note: Object.assign({}, nextProps.note) note: Object.assign({linesHighlighted: []}, nextProps.note)
}, () => { }, () => {
this.refs.content.reload() this.refs.content.reload()
if (this.refs.tags) this.refs.tags.reset() if (this.refs.tags) this.refs.tags.reset()
@@ -361,6 +363,7 @@ class MarkdownNoteDetail extends React.Component {
value={note.content} value={note.content}
storageKey={note.storage} storageKey={note.storage}
noteKey={note.key} noteKey={note.key}
linesHighlighted={note.linesHighlighted}
onChange={this.handleUpdateContent.bind(this)} onChange={this.handleUpdateContent.bind(this)}
ignorePreviewPointerEvents={ignorePreviewPointerEvents} ignorePreviewPointerEvents={ignorePreviewPointerEvents}
/> />
@@ -371,6 +374,7 @@ class MarkdownNoteDetail extends React.Component {
value={note.content} value={note.content}
storageKey={note.storage} storageKey={note.storage}
noteKey={note.key} noteKey={note.key}
linesHighlighted={note.linesHighlighted}
onChange={this.handleUpdateContent.bind(this)} onChange={this.handleUpdateContent.bind(this)}
ignorePreviewPointerEvents={ignorePreviewPointerEvents} ignorePreviewPointerEvents={ignorePreviewPointerEvents}
/> />

View File

@@ -48,7 +48,7 @@ class SnippetNoteDetail extends React.Component {
note: Object.assign({ note: Object.assign({
description: '' description: ''
}, props.note, { }, props.note, {
snippets: props.note.snippets.map((snippet) => Object.assign({}, snippet)) snippets: props.note.snippets.map((snippet) => Object.assign({linesHighlighted: []}, snippet))
}) })
} }
@@ -76,8 +76,9 @@ class SnippetNoteDetail extends React.Component {
const nextNote = Object.assign({ const nextNote = Object.assign({
description: '' description: ''
}, nextProps.note, { }, nextProps.note, {
snippets: nextProps.note.snippets.map((snippet) => Object.assign({}, snippet)) snippets: nextProps.note.snippets.map((snippet) => Object.assign({linesHighlighted: []}, snippet))
}) })
this.setState({ this.setState({
snippetIndex: 0, snippetIndex: 0,
note: nextNote note: nextNote
@@ -410,6 +411,8 @@ class SnippetNoteDetail extends React.Component {
return (e) => { return (e) => {
const snippets = this.state.note.snippets.slice() const snippets = this.state.note.snippets.slice()
snippets[index].content = this.refs['code-' + index].value snippets[index].content = this.refs['code-' + index].value
snippets[index].linesHighlighted = e.options.linesHighlighted
this.setState(state => ({note: Object.assign(state.note, {snippets: snippets})})) this.setState(state => ({note: Object.assign(state.note, {snippets: snippets})}))
this.setState(state => ({ this.setState(state => ({
note: state.note note: state.note
@@ -602,7 +605,8 @@ class SnippetNoteDetail extends React.Component {
note.snippets = note.snippets.concat([{ note.snippets = note.snippets.concat([{
name: '', name: '',
mode: config.editor.snippetDefaultLanguage || 'text', mode: config.editor.snippetDefaultLanguage || 'text',
content: '' content: '',
linesHighlighted: []
}]) }])
const snippetIndex = note.snippets.length - 1 const snippetIndex = note.snippets.length - 1
@@ -645,11 +649,18 @@ class SnippetNoteDetail extends React.Component {
if (infoPanel.style) infoPanel.style.display = infoPanel.style.display === 'none' ? 'inline' : 'none' if (infoPanel.style) infoPanel.style.display = infoPanel.style.display === 'none' ? 'inline' : 'none'
} }
showWarning () { showWarning (e, msg) {
const warningMessage = (msg) => ({
'export-txt': 'Text export',
'export-md': 'Markdown export',
'export-html': 'HTML export',
'print': 'Print'
})[msg]
dialog.showMessageBox(remote.getCurrentWindow(), { dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning', type: 'warning',
message: i18n.__('Sorry!'), message: i18n.__('Sorry!'),
detail: i18n.__('md/text import is available only a markdown note.'), detail: i18n.__(warningMessage(msg) + ' is available only in markdown notes.'),
buttons: [i18n.__('OK')] buttons: [i18n.__('OK')]
}) })
} }
@@ -685,10 +696,8 @@ class SnippetNoteDetail extends React.Component {
const viewList = note.snippets.map((snippet, index) => { const viewList = note.snippets.map((snippet, index) => {
const isActive = this.state.snippetIndex === index const isActive = this.state.snippetIndex === index
let syntax = CodeMirror.findModeByName(convertModeName(snippet.mode)) let syntax = CodeMirror.findModeByName(convertModeName(snippet.mode))
if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text') if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text')
return <div styleName='tabView' return <div styleName='tabView'
key={index} key={index}
style={{zIndex: isActive ? 5 : 4}} style={{zIndex: isActive ? 5 : 4}}
@@ -697,6 +706,7 @@ class SnippetNoteDetail extends React.Component {
? <MarkdownEditor styleName='tabView-content' ? <MarkdownEditor styleName='tabView-content'
value={snippet.content} value={snippet.content}
config={config} config={config}
linesHighlighted={snippet.linesHighlighted}
onChange={(e) => this.handleCodeChange(index)(e)} onChange={(e) => this.handleCodeChange(index)(e)}
ref={'code-' + index} ref={'code-' + index}
ignorePreviewPointerEvents={this.props.ignorePreviewPointerEvents} ignorePreviewPointerEvents={this.props.ignorePreviewPointerEvents}
@@ -705,6 +715,7 @@ class SnippetNoteDetail extends React.Component {
: <CodeEditor styleName='tabView-content' : <CodeEditor styleName='tabView-content'
mode={snippet.mode} mode={snippet.mode}
value={snippet.content} value={snippet.content}
linesHighlighted={snippet.linesHighlighted}
theme={config.editor.theme} theme={config.editor.theme}
fontFamily={config.editor.fontFamily} fontFamily={config.editor.fontFamily}
fontSize={editorFontSize} fontSize={editorFontSize}
@@ -717,6 +728,8 @@ class SnippetNoteDetail extends React.Component {
enableTableEditor={config.editor.enableTableEditor} enableTableEditor={config.editor.enableTableEditor}
onChange={(e) => this.handleCodeChange(index)(e)} onChange={(e) => this.handleCodeChange(index)(e)}
ref={'code-' + index} ref={'code-' + index}
enableSmartPaste={config.editor.enableSmartPaste}
hotkey={config.hotkey}
/> />
} }
</div> </div>
@@ -800,7 +813,9 @@ class SnippetNoteDetail extends React.Component {
createdAt={formatDate(note.createdAt)} createdAt={formatDate(note.createdAt)}
exportAsMd={this.showWarning} exportAsMd={this.showWarning}
exportAsTxt={this.showWarning} exportAsTxt={this.showWarning}
exportAsHtml={this.showWarning}
type={note.type} type={note.type}
print={this.showWarning}
/> />
</div> </div>
</div> </div>

View File

@@ -31,7 +31,7 @@
.tabList .tabList
absolute left right absolute left right
top 55px top 70px
height 30px height 30px
display flex display flex
background-color $ui-noteDetail-backgroundColor background-color $ui-noteDetail-backgroundColor
@@ -57,6 +57,9 @@
.tabList .tabButton .tabList .tabButton
navWhiteButtonColor() navWhiteButtonColor()
width 30px width 30px
border-left 1px solid $ui-borderColor
border-top 1px solid $ui-borderColor
border-right 1px solid $ui-borderColor
.tabView .tabView
absolute left right bottom absolute left right bottom
@@ -98,17 +101,34 @@
opacity 0 opacity 0
transition 0.1s transition 0.1s
body[data-theme="white"] body[data-theme="white"], body[data-theme="default"]
.root .root
box-shadow $note-detail-box-shadow box-shadow $note-detail-box-shadow
border none border none
.tabButton
&:hover
background-color alpha($ui-button--active-backgroundColor, 20%)
color $ui-text-color
transition 0.15s
body[data-theme="dark"] body[data-theme="dark"]
.root .root
border-left 1px solid $ui-dark-borderColor border-left 1px solid $ui-dark-borderColor
background-color $ui-dark-noteDetail-backgroundColor background-color $ui-dark-noteDetail-backgroundColor
box-shadow none box-shadow none
.tabList .tabButton
border-color $ui-dark-borderColor
&:hover
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
.tabButton
&:hover
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
color $ui-dark-text-color
transition 0.15s
.body .body
background-color $ui-dark-noteDetail-backgroundColor background-color $ui-dark-noteDetail-backgroundColor
@@ -118,7 +138,6 @@ body[data-theme="dark"]
border 1px solid $ui-dark-borderColor border 1px solid $ui-dark-borderColor
.tabList .tabList
background-color $ui-button--active-backgroundColor
background-color $ui-dark-noteDetail-backgroundColor background-color $ui-dark-noteDetail-backgroundColor
.tabList .list .tabList .list
@@ -150,6 +169,15 @@ body[data-theme="solarized-dark"]
color $ui-solarized-dark-text-color color $ui-solarized-dark-text-color
border 1px solid $ui-solarized-dark-borderColor border 1px solid $ui-solarized-dark-borderColor
.tabList .tabButton
border-color $ui-solarized-dark-borderColor
.tabButton
&:hover
color $ui-solarized-dark-button--active-color
background-color $ui-solarized-dark-noteDetail-backgroundColor
transition 0.15s
.tabList .tabList
background-color $ui-solarized-dark-noteDetail-backgroundColor background-color $ui-solarized-dark-noteDetail-backgroundColor
color $ui-solarized-dark-text-color color $ui-solarized-dark-text-color
@@ -167,6 +195,14 @@ body[data-theme="monokai"]
color $ui-monokai-text-color color $ui-monokai-text-color
border 1px solid $ui-monokai-borderColor border 1px solid $ui-monokai-borderColor
.tabList .tabButton
border-color $ui-monokai-borderColor
.tabButton
&:hover
color $ui-monokai-text-color
background-color $ui-monokai-noteDetail-backgroundColor
.tabList .tabList
background-color $ui-monokai-noteDetail-backgroundColor background-color $ui-monokai-noteDetail-backgroundColor
color $ui-monokai-text-color color $ui-monokai-text-color
@@ -184,6 +220,14 @@ body[data-theme="dracula"]
color $ui-dracula-text-color color $ui-dracula-text-color
border 1px solid $ui-dracula-borderColor border 1px solid $ui-dracula-borderColor
.tabList .tabButton
border-color $ui-dracula-borderColor
.tabButton
&:hover
color $ui-dracula-text-color
background-color $ui-dracula-noteDetail-backgroundColor
.tabList .tabList
background-color $ui-dracula-noteDetail-backgroundColor background-color $ui-dracula-noteDetail-backgroundColor
color $ui-dracula-text-color color $ui-dracula-text-color

View File

@@ -96,12 +96,14 @@ class Main extends React.Component {
{ {
name: 'example.html', name: 'example.html',
mode: 'html', mode: 'html',
content: "<html>\n<body>\n<h1 id='hello'>Enjoy Boostnote!</h1>\n</body>\n</html>" content: "<html>\n<body>\n<h1 id='hello'>Enjoy Boostnote!</h1>\n</body>\n</html>",
linesHighlighted: []
}, },
{ {
name: 'example.js', name: 'example.js',
mode: 'javascript', mode: 'javascript',
content: "var boostnote = document.getElementById('enjoy').innerHTML\n\nconsole.log(boostnote)" content: "var boostnote = document.getElementById('enjoy').innerHTML\n\nconsole.log(boostnote)",
linesHighlighted: []
} }
] ]
}) })
@@ -234,8 +236,8 @@ class Main extends React.Component {
if (this.state.isRightSliderFocused) { if (this.state.isRightSliderFocused) {
const offset = this.refs.body.getBoundingClientRect().left const offset = this.refs.body.getBoundingClientRect().left
let newListWidth = e.pageX - offset let newListWidth = e.pageX - offset
if (newListWidth < 10) { if (newListWidth < 180) {
newListWidth = 10 newListWidth = 180
} else if (newListWidth > 600) { } else if (newListWidth > 600) {
newListWidth = 600 newListWidth = 600
} }

View File

@@ -2,7 +2,6 @@
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import React from 'react' import React from 'react'
import CSSModules from 'browser/lib/CSSModules' import CSSModules from 'browser/lib/CSSModules'
import debounceRender from 'react-debounce-render'
import styles from './NoteList.styl' import styles from './NoteList.styl'
import moment from 'moment' import moment from 'moment'
import _ from 'lodash' import _ from 'lodash'
@@ -64,13 +63,14 @@ class NoteList extends React.Component {
this.focusHandler = () => { this.focusHandler = () => {
this.refs.list.focus() this.refs.list.focus()
} }
this.alertIfSnippetHandler = () => { this.alertIfSnippetHandler = (event, msg) => {
this.alertIfSnippet() this.alertIfSnippet(msg)
} }
this.importFromFileHandler = this.importFromFile.bind(this) this.importFromFileHandler = this.importFromFile.bind(this)
this.jumpNoteByHash = this.jumpNoteByHashHandler.bind(this) this.jumpNoteByHash = this.jumpNoteByHashHandler.bind(this)
this.handleNoteListKeyUp = this.handleNoteListKeyUp.bind(this) this.handleNoteListKeyUp = this.handleNoteListKeyUp.bind(this)
this.getNoteKeyFromTargetIndex = this.getNoteKeyFromTargetIndex.bind(this) this.getNoteKeyFromTargetIndex = this.getNoteKeyFromTargetIndex.bind(this)
this.cloneNote = this.cloneNote.bind(this)
this.deleteNote = this.deleteNote.bind(this) this.deleteNote = this.deleteNote.bind(this)
this.focusNote = this.focusNote.bind(this) this.focusNote = this.focusNote.bind(this)
this.pinToTop = this.pinToTop.bind(this) this.pinToTop = this.pinToTop.bind(this)
@@ -96,6 +96,7 @@ class NoteList extends React.Component {
this.refreshTimer = setInterval(() => this.forceUpdate(), 60 * 1000) this.refreshTimer = setInterval(() => this.forceUpdate(), 60 * 1000)
ee.on('list:next', this.selectNextNoteHandler) ee.on('list:next', this.selectNextNoteHandler)
ee.on('list:prior', this.selectPriorNoteHandler) ee.on('list:prior', this.selectPriorNoteHandler)
ee.on('list:clone', this.cloneNote)
ee.on('list:focus', this.focusHandler) ee.on('list:focus', this.focusHandler)
ee.on('list:isMarkdownNote', this.alertIfSnippetHandler) ee.on('list:isMarkdownNote', this.alertIfSnippetHandler)
ee.on('import:file', this.importFromFileHandler) ee.on('import:file', this.importFromFileHandler)
@@ -118,6 +119,7 @@ class NoteList extends React.Component {
ee.off('list:next', this.selectNextNoteHandler) ee.off('list:next', this.selectNextNoteHandler)
ee.off('list:prior', this.selectPriorNoteHandler) ee.off('list:prior', this.selectPriorNoteHandler)
ee.off('list:clone', this.cloneNote)
ee.off('list:focus', this.focusHandler) ee.off('list:focus', this.focusHandler)
ee.off('list:isMarkdownNote', this.alertIfSnippetHandler) ee.off('list:isMarkdownNote', this.alertIfSnippetHandler)
ee.off('import:file', this.importFromFileHandler) ee.off('import:file', this.importFromFileHandler)
@@ -173,16 +175,15 @@ class NoteList extends React.Component {
} }
} }
focusNote (selectedNoteKeys, noteKey) { focusNote (selectedNoteKeys, noteKey, pathname) {
const { router } = this.context const { router } = this.context
const { location } = this.props
this.setState({ this.setState({
selectedNoteKeys selectedNoteKeys
}) })
router.push({ router.push({
pathname: location.pathname, pathname,
query: { query: {
key: noteKey key: noteKey
} }
@@ -201,6 +202,7 @@ class NoteList extends React.Component {
} }
let { selectedNoteKeys } = this.state let { selectedNoteKeys } = this.state
const { shiftKeyDown } = this.state const { shiftKeyDown } = this.state
const { location } = this.props
let targetIndex = this.getTargetIndex() let targetIndex = this.getTargetIndex()
@@ -217,7 +219,7 @@ class NoteList extends React.Component {
selectedNoteKeys.push(priorNoteKey) selectedNoteKeys.push(priorNoteKey)
} }
this.focusNote(selectedNoteKeys, priorNoteKey) this.focusNote(selectedNoteKeys, priorNoteKey, location.pathname)
ee.emit('list:moved') ee.emit('list:moved')
} }
@@ -228,6 +230,7 @@ class NoteList extends React.Component {
} }
let { selectedNoteKeys } = this.state let { selectedNoteKeys } = this.state
const { shiftKeyDown } = this.state const { shiftKeyDown } = this.state
const { location } = this.props
let targetIndex = this.getTargetIndex() let targetIndex = this.getTargetIndex()
const isTargetLastNote = targetIndex === this.notes.length - 1 const isTargetLastNote = targetIndex === this.notes.length - 1
@@ -250,7 +253,7 @@ class NoteList extends React.Component {
selectedNoteKeys.push(nextNoteKey) selectedNoteKeys.push(nextNoteKey)
} }
this.focusNote(selectedNoteKeys, nextNoteKey) this.focusNote(selectedNoteKeys, nextNoteKey, location.pathname)
ee.emit('list:moved') ee.emit('list:moved')
} }
@@ -262,7 +265,7 @@ class NoteList extends React.Component {
} }
const selectedNoteKeys = [noteHash] const selectedNoteKeys = [noteHash]
this.focusNote(selectedNoteKeys, noteHash) this.focusNote(selectedNoteKeys, noteHash, '/home')
ee.emit('list:moved') ee.emit('list:moved')
} }
@@ -276,12 +279,6 @@ class NoteList extends React.Component {
ee.emit('top:new-note') ee.emit('top:new-note')
} }
// D key
if (e.keyCode === 68) {
e.preventDefault()
this.deleteNote()
}
// E key // E key
if (e.keyCode === 69) { if (e.keyCode === 69) {
e.preventDefault() e.preventDefault()
@@ -494,14 +491,21 @@ class NoteList extends React.Component {
}) })
} }
alertIfSnippet () { alertIfSnippet (msg) {
const warningMessage = (msg) => ({
'export-txt': 'Text export',
'export-md': 'Markdown export',
'export-html': 'HTML export',
'print': 'Print'
})[msg]
const targetIndex = this.getTargetIndex() const targetIndex = this.getTargetIndex()
if (this.notes[targetIndex].type === 'SNIPPET_NOTE') { if (this.notes[targetIndex].type === 'SNIPPET_NOTE') {
dialog.showMessageBox(remote.getCurrentWindow(), { dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning', type: 'warning',
message: i18n.__('Sorry!'), message: i18n.__('Sorry!'),
detail: i18n.__('md/text import is available only a markdown note.'), detail: i18n.__(warningMessage(msg) + ' is available only in markdown notes.'),
buttons: [i18n.__('OK'), i18n.__('Cancel')] buttons: [i18n.__('OK')]
}) })
} }
} }
@@ -706,7 +710,8 @@ class NoteList extends React.Component {
type: firstNote.type, type: firstNote.type,
folder: folder.key, folder: folder.key,
title: firstNote.title + ' ' + i18n.__('copy'), title: firstNote.title + ' ' + i18n.__('copy'),
content: firstNote.content content: firstNote.content,
linesHighlighted: firstNote.linesHighlighted
}) })
.then((note) => { .then((note) => {
attachmentManagement.cloneAttachments(firstNote, note) attachmentManagement.cloneAttachments(firstNote, note)
@@ -1124,4 +1129,4 @@ NoteList.propTypes = {
}) })
} }
export default debounceRender(CSSModules(NoteList, styles)) export default CSSModules(NoteList, styles)

View File

@@ -25,7 +25,8 @@ class StorageItem extends React.Component {
const { storage } = this.props const { storage } = this.props
this.state = { this.state = {
isOpen: !!storage.isOpen isOpen: !!storage.isOpen,
draggedOver: null
} }
} }
@@ -204,6 +205,20 @@ class StorageItem extends React.Component {
folderKey: data.folderKey, folderKey: data.folderKey,
fileType: data.fileType fileType: data.fileType
}) })
return data
})
.then(data => {
dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'info',
message: 'Exported to "' + data.exportDir + '"'
})
})
.catch(err => {
dialog.showErrorBox(
'Export error',
err ? err.message || err : 'Unexpected error during export'
)
throw err
}) })
} }
}) })
@@ -231,14 +246,20 @@ class StorageItem extends React.Component {
} }
} }
handleDragEnter (e) { handleDragEnter (e, key) {
e.dataTransfer.setData('defaultColor', e.target.style.backgroundColor) e.preventDefault()
e.target.style.backgroundColor = 'rgba(129, 130, 131, 0.08)' if (this.state.draggedOver === key) { return }
this.setState({
draggedOver: key
})
} }
handleDragLeave (e) { handleDragLeave (e) {
e.target.style.opacity = '1' e.preventDefault()
e.target.style.backgroundColor = e.dataTransfer.getData('defaultColor') if (this.state.draggedOver === null) { return }
this.setState({
draggedOver: null
})
} }
dropNote (storage, folder, dispatch, location, noteData) { dropNote (storage, folder, dispatch, location, noteData) {
@@ -263,8 +284,12 @@ class StorageItem extends React.Component {
} }
handleDrop (e, storage, folder, dispatch, location) { handleDrop (e, storage, folder, dispatch, location) {
e.target.style.opacity = '1' e.preventDefault()
e.target.style.backgroundColor = e.dataTransfer.getData('defaultColor') if (this.state.draggedOver !== null) {
this.setState({
draggedOver: null
})
}
const noteData = JSON.parse(e.dataTransfer.getData('note')) const noteData = JSON.parse(e.dataTransfer.getData('note'))
this.dropNote(storage, folder, dispatch, location, noteData) this.dropNote(storage, folder, dispatch, location, noteData)
} }
@@ -291,16 +316,22 @@ class StorageItem extends React.Component {
<SortableStorageItemChild <SortableStorageItemChild
key={folder.key} key={folder.key}
index={index} index={index}
isActive={isActive} isActive={isActive || folder.key === this.state.draggedOver}
handleButtonClick={(e) => this.handleFolderButtonClick(folder.key)(e)} handleButtonClick={(e) => this.handleFolderButtonClick(folder.key)(e)}
handleContextMenu={(e) => this.handleFolderButtonContextMenu(e, folder)} handleContextMenu={(e) => this.handleFolderButtonContextMenu(e, folder)}
folderName={folder.name} folderName={folder.name}
folderColor={folder.color} folderColor={folder.color}
isFolded={isFolded} isFolded={isFolded}
noteCount={noteCount} noteCount={noteCount}
handleDrop={(e) => this.handleDrop(e, storage, folder, dispatch, location)} handleDrop={(e) => {
handleDragEnter={this.handleDragEnter} this.handleDrop(e, storage, folder, dispatch, location)
handleDragLeave={this.handleDragLeave} }}
handleDragEnter={(e) => {
this.handleDragEnter(e, folder.key)
}}
handleDragLeave={(e) => {
this.handleDragLeave(e, folder)
}}
/> />
) )
}) })

View File

@@ -6,6 +6,7 @@ import _ from 'lodash'
import ee from 'browser/main/lib/eventEmitter' import ee from 'browser/main/lib/eventEmitter'
import NewNoteButton from 'browser/main/NewNoteButton' import NewNoteButton from 'browser/main/NewNoteButton'
import i18n from 'browser/lib/i18n' import i18n from 'browser/lib/i18n'
import debounce from 'lodash/debounce'
class TopBar extends React.Component { class TopBar extends React.Component {
constructor (props) { constructor (props) {
@@ -25,6 +26,10 @@ class TopBar extends React.Component {
} }
this.codeInitHandler = this.handleCodeInit.bind(this) this.codeInitHandler = this.handleCodeInit.bind(this)
this.updateKeyword = debounce(this.updateKeyword, 1000 / 60, {
maxWait: 1000 / 8
})
} }
componentDidMount () { componentDidMount () {
@@ -94,7 +99,6 @@ class TopBar extends React.Component {
} }
handleKeyUp (e) { handleKeyUp (e) {
const { router } = this.context
// reset states // reset states
this.setState({ this.setState({
isConfirmTranslation: false isConfirmTranslation: false
@@ -106,21 +110,21 @@ class TopBar extends React.Component {
isConfirmTranslation: true isConfirmTranslation: true
}) })
const keyword = this.refs.searchInput.value const keyword = this.refs.searchInput.value
router.push(`/searched/${encodeURIComponent(keyword)}`) this.updateKeyword(keyword)
this.setState({
search: keyword
})
} }
} }
handleSearchChange (e) { handleSearchChange (e) {
const { router } = this.context
const keyword = this.refs.searchInput.value
if (this.state.isAlphabet || this.state.isConfirmTranslation) { if (this.state.isAlphabet || this.state.isConfirmTranslation) {
router.push(`/searched/${encodeURIComponent(keyword)}`) const keyword = this.refs.searchInput.value
this.updateKeyword(keyword)
} else { } else {
e.preventDefault() e.preventDefault()
} }
}
updateKeyword (keyword) {
this.context.router.push(`/searched/${encodeURIComponent(keyword)}`)
this.setState({ this.setState({
search: keyword search: keyword
}) })

View File

@@ -25,7 +25,8 @@ export const DEFAULT_CONFIG = {
hotkey: { hotkey: {
toggleMain: OSX ? 'Command + Alt + L' : 'Super + Alt + E', toggleMain: OSX ? 'Command + Alt + L' : 'Super + Alt + E',
toggleMode: OSX ? 'Command + Alt + M' : 'Ctrl + M', toggleMode: OSX ? 'Command + Alt + M' : 'Ctrl + M',
deleteNote: OSX ? 'Command + Shift + Backspace' : 'Ctrl + Shift + Backspace' deleteNote: OSX ? 'Command + Shift + Backspace' : 'Ctrl + Shift + Backspace',
pasteSmartly: OSX ? 'Command + Shift + V' : 'Ctrl + Shift + V'
}, },
ui: { ui: {
language: 'en', language: 'en',
@@ -52,7 +53,8 @@ export const DEFAULT_CONFIG = {
enableTableEditor: false, enableTableEditor: false,
enableFrontMatterTitle: true, enableFrontMatterTitle: true,
frontMatterTitleField: 'title', frontMatterTitleField: 'title',
spellcheck: false spellcheck: false,
enableSmartPaste: false
}, },
preview: { preview: {
fontSize: '14', fontSize: '14',

View File

@@ -227,7 +227,15 @@ function migrateAttachments (markdownContent, storagePath, noteKey) {
* @returns {String} postprocessed HTML in which all :storage references are mapped to the actual paths. * @returns {String} postprocessed HTML in which all :storage references are mapped to the actual paths.
*/ */
function fixLocalURLS (renderedHTML, storagePath) { function fixLocalURLS (renderedHTML, storagePath) {
return renderedHTML.replace(new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER + '.*?"', 'g'), function (match) { /*
A :storage reference is like `:storage/3b6f8bd6-4edd-4b15-96e0-eadc4475b564/f939b2c3.jpg`.
- `STORAGE_FOLDER_PLACEHOLDER` will match `:storage`
- `(?:(?:\\\/|%5C)[\\w.]+)+` will match `/3b6f8bd6-4edd-4b15-96e0-eadc4475b564/f939b2c3.jpg`
- `(?:\\\/|%5C)[\\w.]+` will either match `/3b6f8bd6-4edd-4b15-96e0-eadc4475b564` or `/f939b2c3.jpg`
- `(?:\\\/|%5C)` match the path seperator. `\\\/` for posix systems and `%5C` for windows.
*/
return renderedHTML.replace(new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER + '(?:(?:\\\/|%5C)[\\w.]+)+', 'g'), function (match) {
var encodedPathSeparators = new RegExp(mdurl.encode(path.win32.sep) + '|' + mdurl.encode(path.posix.sep), 'g') var encodedPathSeparators = new RegExp(mdurl.encode(path.win32.sep) + '|' + mdurl.encode(path.posix.sep), 'g')
return match.replace(encodedPathSeparators, path.sep).replace(new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER, 'g'), 'file:///' + path.join(storagePath, DESTINATION_FOLDER)) return match.replace(encodedPathSeparators, path.sep).replace(new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER, 'g'), 'file:///' + path.join(storagePath, DESTINATION_FOLDER))
}) })
@@ -316,6 +324,44 @@ function handlePastImageEvent (codeEditor, storageKey, noteKey, dataTransferItem
reader.readAsDataURL(blob) reader.readAsDataURL(blob)
} }
/**
* @description Creates a new file in the storage folder belonging to the current note and inserts the correct markdown code
* @param {CodeEditor} codeEditor Markdown editor. Its insertAttachmentMd() method will be called to include the markdown code
* @param {String} storageKey Key of the current storage
* @param {String} noteKey Key of the current note
* @param {NativeImage} image The native image
*/
function handlePastNativeImage (codeEditor, storageKey, noteKey, image) {
if (!codeEditor) {
throw new Error('codeEditor has to be given')
}
if (!storageKey) {
throw new Error('storageKey has to be given')
}
if (!noteKey) {
throw new Error('noteKey has to be given')
}
if (!image) {
throw new Error('image has to be given')
}
const targetStorage = findStorage.findStorage(storageKey)
const destinationDir = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey)
createAttachmentDestinationFolder(targetStorage.path, noteKey)
const imageName = `${uniqueSlug()}.png`
const imagePath = path.join(destinationDir, imageName)
const binaryData = image.toPNG()
fs.writeFileSync(imagePath, binaryData, 'binary')
const imageReferencePath = path.join(STORAGE_FOLDER_PLACEHOLDER, noteKey, imageName)
const imageMd = generateAttachmentMarkdown(imageName, imageReferencePath, true)
codeEditor.insertAttachmentMd(imageMd)
}
/** /**
* @description Returns all attachment paths of the given markdown * @description Returns all attachment paths of the given markdown
* @param {String} markdownContent content in which the attachment paths should be found * @param {String} markdownContent content in which the attachment paths should be found
@@ -539,6 +585,7 @@ module.exports = {
generateAttachmentMarkdown, generateAttachmentMarkdown,
handleAttachmentDrop, handleAttachmentDrop,
handlePastImageEvent, handlePastImageEvent,
handlePastNativeImage,
getAttachmentsInMarkdownContent, getAttachmentsInMarkdownContent,
getAbsolutePathsOfAttachmentsInContent, getAbsolutePathsOfAttachmentsInContent,
removeStorageAndNoteReferences, removeStorageAndNoteReferences,

View File

@@ -16,7 +16,7 @@ function copyFile (srcPath, dstPath) {
const dstFolder = path.dirname(dstPath) const dstFolder = path.dirname(dstPath)
if (!fs.existsSync(dstFolder)) fs.mkdirSync(dstFolder) if (!fs.existsSync(dstFolder)) fs.mkdirSync(dstFolder)
const input = fs.createReadStream(srcPath) const input = fs.createReadStream(decodeURI(srcPath))
const output = fs.createWriteStream(dstPath) const output = fs.createWriteStream(dstPath)
output.on('error', reject) output.on('error', reject)

View File

@@ -16,6 +16,7 @@ function validateInput (input) {
switch (input.type) { switch (input.type) {
case 'MARKDOWN_NOTE': case 'MARKDOWN_NOTE':
if (!_.isString(input.content)) input.content = '' if (!_.isString(input.content)) input.content = ''
if (!_.isArray(input.linesHighlighted)) input.linesHighlighted = []
break break
case 'SNIPPET_NOTE': case 'SNIPPET_NOTE':
if (!_.isString(input.description)) input.description = '' if (!_.isString(input.description)) input.description = ''
@@ -23,7 +24,8 @@ function validateInput (input) {
input.snippets = [{ input.snippets = [{
name: '', name: '',
mode: 'text', mode: 'text',
content: '' content: '',
linesHighlighted: []
}] }]
} }
break break

View File

@@ -9,7 +9,8 @@ function createSnippet (snippetFile) {
id: crypto.randomBytes(16).toString('hex'), id: crypto.randomBytes(16).toString('hex'),
name: 'Unnamed snippet', name: 'Unnamed snippet',
prefix: [], prefix: [],
content: '' content: '',
linesHighlighted: []
} }
fetchSnippet(null, snippetFile).then((snippets) => { fetchSnippet(null, snippetFile).then((snippets) => {
snippets.push(newSnippet) snippets.push(newSnippet)

View File

@@ -1,9 +1,9 @@
import { findStorage } from 'browser/lib/findStorage' import { findStorage } from 'browser/lib/findStorage'
import resolveStorageData from './resolveStorageData' import resolveStorageData from './resolveStorageData'
import resolveStorageNotes from './resolveStorageNotes' import resolveStorageNotes from './resolveStorageNotes'
import exportNote from './exportNote'
import filenamify from 'filenamify' import filenamify from 'filenamify'
import * as path from 'path' import * as path from 'path'
import * as fs from 'fs'
/** /**
* @param {String} storageKey * @param {String} storageKey
@@ -45,9 +45,9 @@ function exportFolder (storageKey, folderKey, fileType, exportDir) {
notes notes
.filter(note => note.folder === folderKey && note.isTrashed === false && note.type === 'MARKDOWN_NOTE') .filter(note => note.folder === folderKey && note.isTrashed === false && note.type === 'MARKDOWN_NOTE')
.forEach(snippet => { .forEach(note => {
const notePath = path.join(exportDir, `${filenamify(snippet.title, {replacement: '_'})}.${fileType}`) const notePath = path.join(exportDir, `${filenamify(note.title, {replacement: '_'})}.${fileType}`)
fs.writeFileSync(notePath, snippet.content) exportNote(note.key, storage.path, note.content, notePath, null)
}) })
return { return {

View File

@@ -4,27 +4,43 @@ import { findStorage } from 'browser/lib/findStorage'
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const attachmentManagement = require('./attachmentManagement')
/** /**
* Export note together with images * Export note together with attachments
* *
* If images is stored in the storage, creates 'images' subfolder in target directory * If attachments are stored in the storage, creates 'attachments' subfolder in target directory
* and copies images to it. Changes links to images in the content of the note * and copies attachments to it. Changes links to images in the content of the note
* *
* @param {String} nodeKey key of the node that should be exported
* @param {String} storageKey or storage path * @param {String} storageKey or storage path
* @param {String} noteContent Content to export * @param {String} noteContent Content to export
* @param {String} targetPath Path to exported file * @param {String} targetPath Path to exported file
* @param {function} outputFormatter * @param {function} outputFormatter
* @return {Promise.<*[]>} * @return {Promise.<*[]>}
*/ */
function exportNote (storageKey, noteContent, targetPath, outputFormatter) { function exportNote (nodeKey, storageKey, noteContent, targetPath, outputFormatter) {
const storagePath = path.isAbsolute(storageKey) ? storageKey : findStorage(storageKey).path const storagePath = path.isAbsolute(storageKey) ? storageKey : findStorage(storageKey).path
const exportTasks = [] const exportTasks = []
if (!storagePath) { if (!storagePath) {
throw new Error('Storage path is not found') throw new Error('Storage path is not found')
} }
const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent(
noteContent,
storagePath
)
attachmentsAbsolutePaths.forEach(attachment => {
exportTasks.push({
src: attachment,
dst: attachmentManagement.DESTINATION_FOLDER
})
})
let exportedData = noteContent let exportedData = attachmentManagement.removeStorageAndNoteReferences(
noteContent,
nodeKey
)
if (outputFormatter) { if (outputFormatter) {
exportedData = outputFormatter(exportedData, exportTasks) exportedData = outputFormatter(exportedData, exportTasks)

View File

@@ -69,7 +69,8 @@ function importAll (storage, data) {
isStarred: false, isStarred: false,
title: article.title, title: article.title,
content: '# ' + article.title + '\n\n' + article.content, content: '# ' + article.title + '\n\n' + article.content,
key: noteKey key: noteKey,
linesHighlighted: article.linesHighlighted
} }
notes.push(newNote) notes.push(newNote)
} else { } else {
@@ -87,7 +88,8 @@ function importAll (storage, data) {
snippets: [{ snippets: [{
name: article.mode, name: article.mode,
mode: article.mode, mode: article.mode,
content: article.content content: article.content,
linesHighlighted: article.linesHighlighted
}] }]
} }
notes.push(newNote) notes.push(newNote)

View File

@@ -39,6 +39,9 @@ function validateInput (input) {
if (input.content != null) { if (input.content != null) {
if (!_.isString(input.content)) validatedInput.content = '' if (!_.isString(input.content)) validatedInput.content = ''
else validatedInput.content = input.content else validatedInput.content = input.content
if (!_.isArray(input.linesHighlighted)) validatedInput.linesHighlighted = []
else validatedInput.linesHighlighted = input.linesHighlighted
} }
return validatedInput return validatedInput
case 'SNIPPET_NOTE': case 'SNIPPET_NOTE':
@@ -51,7 +54,8 @@ function validateInput (input) {
validatedInput.snippets = [{ validatedInput.snippets = [{
name: '', name: '',
mode: 'text', mode: 'text',
content: '' content: '',
linesHighlighted: []
}] }]
} else { } else {
validatedInput.snippets = input.snippets validatedInput.snippets = input.snippets
@@ -96,12 +100,14 @@ function updateNote (storageKey, noteKey, input) {
snippets: [{ snippets: [{
name: '', name: '',
mode: 'text', mode: 'text',
content: '' content: '',
linesHighlighted: []
}] }]
} }
: { : {
type: 'MARKDOWN_NOTE', type: 'MARKDOWN_NOTE',
content: '' content: '',
linesHighlighted: []
} }
noteData.title = '' noteData.title = ''
if (storage.folders.length === 0) throw new Error('Failed to restore note: No folder exists.') if (storage.folders.length === 0) throw new Error('Failed to restore note: No folder exists.')

View File

@@ -12,7 +12,8 @@ function updateSnippet (snippet, snippetFile) {
if ( if (
currentSnippet.name === snippet.name && currentSnippet.name === snippet.name &&
currentSnippet.prefix === snippet.prefix && currentSnippet.prefix === snippet.prefix &&
currentSnippet.content === snippet.content currentSnippet.content === snippet.content &&
currentSnippet.linesHighlighted === snippet.linesHighlighted
) { ) {
// if everything is the same then don't write to disk // if everything is the same then don't write to disk
resolve(snippets) resolve(snippets)
@@ -20,6 +21,7 @@ function updateSnippet (snippet, snippetFile) {
currentSnippet.name = snippet.name currentSnippet.name = snippet.name
currentSnippet.prefix = snippet.prefix currentSnippet.prefix = snippet.prefix
currentSnippet.content = snippet.content currentSnippet.content = snippet.content
currentSnippet.linesHighlighted = (snippet.linesHighlighted)
fs.writeFile(snippetFile || consts.SNIPPET_FILE, JSON.stringify(snippets, null, 4), (err) => { fs.writeFile(snippetFile || consts.SNIPPET_FILE, JSON.stringify(snippets, null, 4), (err) => {
if (err) reject(err) if (err) reject(err)
resolve(snippets) resolve(snippets)

View File

@@ -79,7 +79,8 @@ class HotkeyTab extends React.Component {
config.hotkey = { config.hotkey = {
toggleMain: this.refs.toggleMain.value, toggleMain: this.refs.toggleMain.value,
toggleMode: this.refs.toggleMode.value, toggleMode: this.refs.toggleMode.value,
deleteNote: this.refs.deleteNote.value deleteNote: this.refs.deleteNote.value,
pasteSmartly: this.refs.pasteSmartly.value
} }
this.setState({ this.setState({
config config
@@ -149,6 +150,17 @@ class HotkeyTab extends React.Component {
/> />
</div> </div>
</div> </div>
<div styleName='group-section'>
<div styleName='group-section-label'>{i18n.__('Paste Smartly')}</div>
<div styleName='group-section-control'>
<input styleName='group-section-control-input'
onChange={(e) => this.handleHotkeyChange(e)}
ref='pasteSmartly'
value={config.hotkey.pasteSmartly}
type='text'
/>
</div>
</div>
<div styleName='group-control'> <div styleName='group-control'>
<button styleName='group-control-leftButton' <button styleName='group-control-leftButton'
onClick={(e) => this.handleHintToggleButtonClick(e)} onClick={(e) => this.handleHintToggleButtonClick(e)}

View File

@@ -73,6 +73,11 @@ class InfoTab extends React.Component {
<div styleName='header--sub'>{i18n.__('Community')}</div> <div styleName='header--sub'>{i18n.__('Community')}</div>
<div styleName='top'> <div styleName='top'>
<ul styleName='list'> <ul styleName='list'>
<li>
<a href='https://issuehunt.io/repos/53266139'
onClick={(e) => this.handleLinkClick(e)}
>{i18n.__('Bounty on IssueHunt')}</a>
</li>
<li> <li>
<a href='https://boostnote.io/#subscribe' <a href='https://boostnote.io/#subscribe'
onClick={(e) => this.handleLinkClick(e)} onClick={(e) => this.handleLinkClick(e)}

View File

@@ -96,7 +96,8 @@ class UiTab extends React.Component {
enableTableEditor: this.refs.enableTableEditor.checked, enableTableEditor: this.refs.enableTableEditor.checked,
enableFrontMatterTitle: this.refs.enableFrontMatterTitle.checked, enableFrontMatterTitle: this.refs.enableFrontMatterTitle.checked,
frontMatterTitleField: this.refs.frontMatterTitleField.value, frontMatterTitleField: this.refs.frontMatterTitleField.value,
spellcheck: this.refs.spellcheck.checked spellcheck: this.refs.spellcheck.checked,
enableSmartPaste: this.refs.enableSmartPaste.checked
}, },
preview: { preview: {
fontSize: this.refs.previewFontSize.value, fontSize: this.refs.previewFontSize.value,
@@ -552,6 +553,18 @@ class UiTab extends React.Component {
{i18n.__('Enable smart table editor')} {i18n.__('Enable smart table editor')}
</label> </label>
</div> </div>
<div styleName='group-checkBoxSection'>
<label>
<input onChange={(e) => this.handleUIChange(e)}
checked={this.state.config.editor.enableSmartPaste}
ref='enableSmartPaste'
type='checkbox'
/>&nbsp;
{i18n.__('Enable smart paste')}
</label>
</div>
<div styleName='group-checkBoxSection'> <div styleName='group-checkBoxSection'>
<label> <label>
<input onChange={(e) => this.handleUIChange(e)} <input onChange={(e) => this.handleUIChange(e)}

View File

@@ -240,10 +240,8 @@ navWhiteButtonColor()
&:hover &:hover
background-color alpha($ui-button--active-backgroundColor, 20%) background-color alpha($ui-button--active-backgroundColor, 20%)
transition 0.15s transition 0.15s
color $ui-text-color
&:active, &:active:hover &:active, &:active:hover
background-color $ui-button--active-backgroundColor background-color $ui-button--active-backgroundColor
color $ui-text-color
transition 0.15s transition 0.15s
// UI Button // UI Button

View File

@@ -85,40 +85,25 @@ const file = {
}, },
{ {
label: 'Focus Note', label: 'Focus Note',
accelerator: 'Control+E', accelerator: macOS ? 'Command+E' : 'Control+E',
click () { click () {
mainWindow.webContents.send('detail:focus') mainWindow.webContents.send('detail:focus')
} }
}, },
{ {
type: 'separator' label: 'Delete Note',
}, accelerator: macOS ? 'Command+Shift+Backspace' : 'Control+Shift+Backspace',
{
label: 'Export as',
submenu: [
{
label: 'Plain Text (.txt)',
click () { click () {
mainWindow.webContents.send('list:isMarkdownNote') mainWindow.webContents.send('detail:delete')
mainWindow.webContents.send('export:save-text')
} }
}, },
{ {
label: 'MarkDown (.md)', label: 'Clone Note',
accelerator: macOS ? 'Command+D' : 'Control+D',
click () { click () {
mainWindow.webContents.send('list:isMarkdownNote') mainWindow.webContents.send('list:clone')
mainWindow.webContents.send('export:save-md')
} }
}, },
{
label: 'HTML (.html)',
click () {
mainWindow.webContents.send('list:isMarkdownNote')
mainWindow.webContents.send('export:save-html')
}
}
]
},
{ {
type: 'separator' type: 'separator'
}, },
@@ -134,14 +119,31 @@ const file = {
] ]
}, },
{ {
type: 'separator' label: 'Export as',
submenu: [
{
label: 'Plain Text (.txt)',
click () {
mainWindow.webContents.send('list:isMarkdownNote', 'export-txt')
mainWindow.webContents.send('export:save-text')
}
}, },
{ {
label: 'Format Table', label: 'MarkDown (.md)',
click () { click () {
mainWindow.webContents.send('code:format-table') mainWindow.webContents.send('list:isMarkdownNote', 'export-md')
mainWindow.webContents.send('export:save-md')
} }
}, },
{
label: 'HTML (.html)',
click () {
mainWindow.webContents.send('list:isMarkdownNote', 'export-html')
mainWindow.webContents.send('export:save-html')
}
}
]
},
{ {
type: 'separator' type: 'separator'
}, },
@@ -153,24 +155,20 @@ const file = {
} }
}, },
{ {
type: 'separator' label: 'Format Table',
},
{
label: 'Print',
accelerator: 'CommandOrControl+P',
click () { click () {
mainWindow.webContents.send('list:isMarkdownNote') mainWindow.webContents.send('code:format-table')
mainWindow.webContents.send('print')
} }
}, },
{ {
type: 'separator' type: 'separator'
}, },
{ {
label: 'Delete Note', label: 'Print',
accelerator: macOS ? 'Control+Backspace' : 'Control+Delete', accelerator: 'CommandOrControl+P',
click () { click () {
mainWindow.webContents.send('detail:delete') mainWindow.webContents.send('list:isMarkdownNote', 'print')
mainWindow.webContents.send('print')
} }
} }
] ]
@@ -296,9 +294,6 @@ const view = {
mainWindow.setFullScreen(!mainWindow.isFullScreen()) mainWindow.setFullScreen(!mainWindow.isFullScreen())
} }
}, },
{
type: 'separator'
},
{ {
label: 'Toggle Side Bar', label: 'Toggle Side Bar',
accelerator: 'CommandOrControl+B', accelerator: 'CommandOrControl+B',

View File

@@ -37,9 +37,15 @@
"White": "白", "White": "白",
"Solarized Dark": "明灰", "Solarized Dark": "明灰",
"Dark": "暗灰", "Dark": "暗灰",
"Default New Note": "新規ノートの形式",
"Always Ask": "作成時に聞く",
"Show a confirmation dialog when deleting notes": "ノートを削除する時に確認ダイアログを表示する", "Show a confirmation dialog when deleting notes": "ノートを削除する時に確認ダイアログを表示する",
"Disable Direct Write (It will be applied after restarting)": "Disable Direct Write (It will be applied after restarting)", "Disable Direct Write (It will be applied after restarting)": "Disable Direct Write (It will be applied after restarting)",
"Save tags of a note in alphabetical order": "ノートのタグをアルファベット順に保存する",
"Show tags of a note in alphabetical order": "ノートのタグをアルファベット順に表示する",
"Show only related tags": "関連するタグのみ表示する", "Show only related tags": "関連するタグのみ表示する",
"Enable live count of notes": "タグ選択時にノート数を再計算して表示する",
"New notes are tagged with the filtering tags": "新規ノートに選択中のタグを付与する",
"Editor Theme": "エディタのテーマ", "Editor Theme": "エディタのテーマ",
"Editor Font Size": "エディタのフォントサイズ", "Editor Font Size": "エディタのフォントサイズ",
"Editor Font Family": "エディタのフォント", "Editor Font Family": "エディタのフォント",
@@ -55,21 +61,28 @@
"vim": "vim", "vim": "vim",
"emacs": "emacs", "emacs": "emacs",
"⚠️ Please restart boostnote after you change the keymap": "⚠️ キーマップ変更後は Boostnote を再起動してください", "⚠️ Please restart boostnote after you change the keymap": "⚠️ キーマップ変更後は Boostnote を再起動してください",
"Snippet Default Language": "スニペットのデフォルト言語",
"Extract title from front matter": "Front matterからタイトルを抽出する",
"Show line numbers in the editor": "エディタ内に行番号を表示", "Show line numbers in the editor": "エディタ内に行番号を表示",
"Allow editor to scroll past the last line": "エディタが最終行以降にスクロールできるようにする", "Allow editor to scroll past the last line": "エディタが最終行以降にスクロールできるようにする",
"Enable smart quotes": "スマートクォートを有効にする", "Enable smart quotes": "スマートクォートを有効にする",
"Bring in web page title when pasting URL on editor": "URLを貼り付けた時にWebページのタイトルを取得する", "Bring in web page title when pasting URL on editor": "URLを貼り付けた時にWebページのタイトルを取得する",
"Enable smart table editor": "スマートテーブルエディタを有効にする",
"Preview": "プレビュー", "Preview": "プレビュー",
"Preview Font Size": "プレビュー時フォントサイズ", "Preview Font Size": "プレビュー時フォントサイズ",
"Preview Font Family": "プレビュー時フォント", "Preview Font Family": "プレビュー時フォント",
"Code Block Theme": "コードブロックのテーマ", "Code Block Theme": "コードブロックのテーマ",
"Allow line through checkbox": "チェック済みチェックボックスのテキストに取り消し線を付与する",
"Allow preview to scroll past the last line": "プレビュー時に最終行以降にスクロールできるようにする", "Allow preview to scroll past the last line": "プレビュー時に最終行以降にスクロールできるようにする",
"When scrolling, synchronize preview with editor": "エディタとプレビューのスクロールを同期する",
"Show line numbers for preview code blocks": "プレビュー時のコードブロック内に行番号を表示する", "Show line numbers for preview code blocks": "プレビュー時のコードブロック内に行番号を表示する",
"LaTeX Inline Open Delimiter": "LaTeX 開始デリミタ(インライン)", "LaTeX Inline Open Delimiter": "LaTeX 開始デリミタ(インライン)",
"LaTeX Inline Close Delimiter": "LaTeX 終了デリミタ(インライン)", "LaTeX Inline Close Delimiter": "LaTeX 終了デリミタ(インライン)",
"LaTeX Block Open Delimiter": "LaTeX 開始デリミタ(ブロック)", "LaTeX Block Open Delimiter": "LaTeX 開始デリミタ(ブロック)",
"LaTeX Block Close Delimiter": "LaTeX 終了デリミタ(ブロック)", "LaTeX Block Close Delimiter": "LaTeX 終了デリミタ(ブロック)",
"PlantUML Server": "PlantUML サーバー", "PlantUML Server": "PlantUML サーバー",
"Custom CSS": "カスタムCSS",
"Allow custom CSS for preview": "プレビュー用のカスタムCSSを許可する",
"Community": "コミュニティ", "Community": "コミュニティ",
"Subscribe to Newsletter": "ニュースレターを購読する", "Subscribe to Newsletter": "ニュースレターを購読する",
"GitHub": "GitHub", "GitHub": "GitHub",
@@ -135,6 +148,7 @@
"Hotkeys": "ホットキー", "Hotkeys": "ホットキー",
"Show/Hide Boostnote": "Boostnote の表示/非表示", "Show/Hide Boostnote": "Boostnote の表示/非表示",
"Toggle Editor Mode": "エディタモードの切替", "Toggle Editor Mode": "エディタモードの切替",
"Delete Note": "ノート削除",
"Restore": "リストア", "Restore": "リストア",
"Permanent Delete": "永久に削除", "Permanent Delete": "永久に削除",
"Confirm note deletion": "ノート削除確認", "Confirm note deletion": "ノート削除確認",

View File

@@ -1,7 +1,7 @@
{ {
"name": "boost", "name": "boost",
"productName": "Boostnote", "productName": "Boostnote",
"version": "0.11.11", "version": "0.11.12",
"main": "index.js", "main": "index.js",
"description": "Boostnote", "description": "Boostnote",
"license": "GPL-3.0", "license": "GPL-3.0",
@@ -62,7 +62,7 @@
"escape-string-regexp": "^1.0.5", "escape-string-regexp": "^1.0.5",
"file-uri-to-path": "^1.0.0", "file-uri-to-path": "^1.0.0",
"file-url": "^2.0.2", "file-url": "^2.0.2",
"filenamify": "^2.0.0", "filenamify": "^2.1.0",
"flowchart.js": "^1.6.5", "flowchart.js": "^1.6.5",
"font-awesome": "^4.3.0", "font-awesome": "^4.3.0",
"fs-extra": "^5.0.0", "fs-extra": "^5.0.0",
@@ -99,8 +99,10 @@
"react-codemirror": "^0.3.0", "react-codemirror": "^0.3.0",
"react-debounce-render": "^4.0.1", "react-debounce-render": "^4.0.1",
"react-dom": "^15.0.2", "react-dom": "^15.0.2",
"react-image-carousel": "^2.0.18",
"react-redux": "^4.4.5", "react-redux": "^4.4.5",
"react-sortable-hoc": "^0.6.7", "react-sortable-hoc": "^0.6.7",
"react-transition-group": "^2.5.0",
"redux": "^3.5.2", "redux": "^3.5.2",
"sander": "^0.5.1", "sander": "^0.5.1",
"sanitize-html": "^1.18.2", "sanitize-html": "^1.18.2",
@@ -142,6 +144,7 @@
"grunt": "^0.4.5", "grunt": "^0.4.5",
"grunt-electron-installer": "2.1.0", "grunt-electron-installer": "2.1.0",
"history": "^1.17.0", "history": "^1.17.0",
"husky": "^1.1.0",
"identity-obj-proxy": "^3.0.0", "identity-obj-proxy": "^3.0.0",
"jest": "^22.4.3", "jest": "^22.4.3",
"jest-localstorage-mock": "^2.2.0", "jest-localstorage-mock": "^2.2.0",
@@ -189,5 +192,10 @@
"<rootDir>/tests/jest.js", "<rootDir>/tests/jest.js",
"jest-localstorage-mock" "jest-localstorage-mock"
] ]
},
"husky": {
"hooks": {
"pre-commit": "npm run lint"
}
} }
} }

View File

@@ -5,8 +5,11 @@
<h4 align="center">Note-taking app for programmers. </h4> <h4 align="center">Note-taking app for programmers. </h4>
<h5 align="center">Apps available for Mac, Windows, Linux, Android, and iOS.</h5> <h5 align="center">Apps available for Mac, Windows, Linux, Android, and iOS.</h5>
<h5 align="center">Built with Electron, React + Redux, Webpack, and CSSModules.</h5> <h5 align="center">Built with Electron, React + Redux, Webpack, and CSSModules.</h5>
<p align="center">
[![Build Status](https://travis-ci.org/BoostIO/Boostnote.svg?branch=master)](https://travis-ci.org/BoostIO/Boostnote) <a href="https://travis-ci.org/BoostIO/Boostnote">
<img src="https://travis-ci.org/BoostIO/Boostnote.svg?branch=master" alt="Build Status" />
</a>
</p>
## Authors & Maintainers ## Authors & Maintainers
- [Rokt33r](https://github.com/rokt33r) - [Rokt33r](https://github.com/rokt33r)
@@ -24,7 +27,7 @@ Boostnote is an open source project. It's an independent project with its ongoin
Issues on Boostnote can be funded by anyone and the money will be distributed to contributors and maintainers. If you use Boostnote please consider becoming a backer: Issues on Boostnote can be funded by anyone and the money will be distributed to contributors and maintainers. If you use Boostnote please consider becoming a backer:
[![issuehunt-image](https://github.com/BoostIO/issuehunt-materials/blob/master/issuehunt-badge@1x.png?raw=true)](https://issuehunt.io/repos/53266139) [![Let's fund issues in this repository](https://issuehunt.io/static/embed/issuehunt-button-v1.svg)](https://issuehunt.io/repos/53266139)
## Community ## Community
- [Facebook Group](https://www.facebook.com/groups/boostnote/) - [Facebook Group](https://www.facebook.com/groups/boostnote/)

View File

@@ -0,0 +1,35 @@
const test = require('ava')
const copyFile = require('browser/main/lib/dataApi/copyFile')
const path = require('path')
const fs = require('fs')
const testFile = 'test.txt'
const srcFolder = path.join(__dirname, '🤔')
const srcPath = path.join(srcFolder, testFile)
const dstFolder = path.join(__dirname, '😇')
const dstPath = path.join(dstFolder, testFile)
test.before((t) => {
if (!fs.existsSync(srcFolder)) fs.mkdirSync(srcFolder)
fs.writeFileSync(srcPath, 'test')
})
test('`copyFile` should handle encoded URI on src path', (t) => {
return copyFile(encodeURI(srcPath), dstPath)
.then(() => {
t.true(true)
})
.catch(() => {
t.true(false)
})
})
test.after((t) => {
fs.unlinkSync(srcPath)
fs.unlinkSync(dstPath)
fs.rmdirSync(srcFolder)
fs.rmdirSync(dstFolder)
})

View File

@@ -25,13 +25,16 @@ test.serial('Create a note', (t) => {
const storageKey = t.context.storage.cache.key const storageKey = t.context.storage.cache.key
const folderKey = t.context.storage.json.folders[0].key const folderKey = t.context.storage.json.folders[0].key
const randLinesHighlightedArray = new Array(10).fill().map(() => Math.round(Math.random() * 10))
const input1 = { const input1 = {
type: 'SNIPPET_NOTE', type: 'SNIPPET_NOTE',
description: faker.lorem.lines(), description: faker.lorem.lines(),
snippets: [{ snippets: [{
name: faker.system.fileName(), name: faker.system.fileName(),
mode: 'text', mode: 'text',
content: faker.lorem.lines() content: faker.lorem.lines(),
linesHighlighted: randLinesHighlightedArray
}], }],
tags: faker.lorem.words().split(' '), tags: faker.lorem.words().split(' '),
folder: folderKey folder: folderKey
@@ -42,7 +45,8 @@ test.serial('Create a note', (t) => {
type: 'MARKDOWN_NOTE', type: 'MARKDOWN_NOTE',
content: faker.lorem.lines(), content: faker.lorem.lines(),
tags: faker.lorem.words().split(' '), tags: faker.lorem.words().split(' '),
folder: folderKey folder: folderKey,
linesHighlighted: randLinesHighlightedArray
} }
input2.title = input2.content.split('\n').shift() input2.title = input2.content.split('\n').shift()
@@ -59,6 +63,7 @@ test.serial('Create a note', (t) => {
t.is(storageKey, data1.storage) t.is(storageKey, data1.storage)
const jsonData1 = CSON.readFileSync(path.join(storagePath, 'notes', data1.key + '.cson')) const jsonData1 = CSON.readFileSync(path.join(storagePath, 'notes', data1.key + '.cson'))
t.is(input1.title, data1.title) t.is(input1.title, data1.title)
t.is(input1.title, jsonData1.title) t.is(input1.title, jsonData1.title)
t.is(input1.description, data1.description) t.is(input1.description, data1.description)
@@ -71,6 +76,8 @@ test.serial('Create a note', (t) => {
t.is(input1.snippets[0].content, jsonData1.snippets[0].content) t.is(input1.snippets[0].content, jsonData1.snippets[0].content)
t.is(input1.snippets[0].name, data1.snippets[0].name) t.is(input1.snippets[0].name, data1.snippets[0].name)
t.is(input1.snippets[0].name, jsonData1.snippets[0].name) t.is(input1.snippets[0].name, jsonData1.snippets[0].name)
t.deepEqual(input1.snippets[0].linesHighlighted, data1.snippets[0].linesHighlighted)
t.deepEqual(input1.snippets[0].linesHighlighted, jsonData1.snippets[0].linesHighlighted)
t.is(storageKey, data2.storage) t.is(storageKey, data2.storage)
const jsonData2 = CSON.readFileSync(path.join(storagePath, 'notes', data2.key + '.cson')) const jsonData2 = CSON.readFileSync(path.join(storagePath, 'notes', data2.key + '.cson'))
@@ -80,6 +87,8 @@ test.serial('Create a note', (t) => {
t.is(input2.content, jsonData2.content) t.is(input2.content, jsonData2.content)
t.is(input2.tags.length, data2.tags.length) t.is(input2.tags.length, data2.tags.length)
t.is(input2.tags.length, jsonData2.tags.length) t.is(input2.tags.length, jsonData2.tags.length)
t.deepEqual(input2.linesHighlighted, data2.linesHighlighted)
t.deepEqual(input2.linesHighlighted, jsonData2.linesHighlighted)
}) })
}) })

View File

@@ -26,6 +26,7 @@ test.serial('Create a snippet', (t) => {
t.is(snippet.name, data.name) t.is(snippet.name, data.name)
t.deepEqual(snippet.prefix, data.prefix) t.deepEqual(snippet.prefix, data.prefix)
t.is(snippet.content, data.content) t.is(snippet.content, data.content)
t.deepEqual(snippet.linesHighlighted, data.linesHighlighted)
}) })
}) })

View File

@@ -26,13 +26,17 @@ test.serial('Update a note', (t) => {
const storageKey = t.context.storage.cache.key const storageKey = t.context.storage.cache.key
const folderKey = t.context.storage.json.folders[0].key const folderKey = t.context.storage.json.folders[0].key
const randLinesHighlightedArray = new Array(10).fill().map(() => Math.round(Math.random() * 10))
const randLinesHighlightedArray2 = new Array(15).fill().map(() => Math.round(Math.random() * 15))
const input1 = { const input1 = {
type: 'SNIPPET_NOTE', type: 'SNIPPET_NOTE',
description: faker.lorem.lines(), description: faker.lorem.lines(),
snippets: [{ snippets: [{
name: faker.system.fileName(), name: faker.system.fileName(),
mode: 'text', mode: 'text',
content: faker.lorem.lines() content: faker.lorem.lines(),
linesHighlighted: randLinesHighlightedArray
}], }],
tags: faker.lorem.words().split(' '), tags: faker.lorem.words().split(' '),
folder: folderKey folder: folderKey
@@ -43,7 +47,8 @@ test.serial('Update a note', (t) => {
type: 'MARKDOWN_NOTE', type: 'MARKDOWN_NOTE',
content: faker.lorem.lines(), content: faker.lorem.lines(),
tags: faker.lorem.words().split(' '), tags: faker.lorem.words().split(' '),
folder: folderKey folder: folderKey,
linesHighlighted: randLinesHighlightedArray
} }
input2.title = input2.content.split('\n').shift() input2.title = input2.content.split('\n').shift()
@@ -53,7 +58,8 @@ test.serial('Update a note', (t) => {
snippets: [{ snippets: [{
name: faker.system.fileName(), name: faker.system.fileName(),
mode: 'text', mode: 'text',
content: faker.lorem.lines() content: faker.lorem.lines(),
linesHighlighted: randLinesHighlightedArray2
}], }],
tags: faker.lorem.words().split(' ') tags: faker.lorem.words().split(' ')
} }
@@ -62,7 +68,8 @@ test.serial('Update a note', (t) => {
const input4 = { const input4 = {
type: 'MARKDOWN_NOTE', type: 'MARKDOWN_NOTE',
content: faker.lorem.lines(), content: faker.lorem.lines(),
tags: faker.lorem.words().split(' ') tags: faker.lorem.words().split(' '),
linesHighlighted: randLinesHighlightedArray2
} }
input4.title = input4.content.split('\n').shift() input4.title = input4.content.split('\n').shift()
@@ -99,6 +106,8 @@ test.serial('Update a note', (t) => {
t.is(input3.snippets[0].content, jsonData1.snippets[0].content) t.is(input3.snippets[0].content, jsonData1.snippets[0].content)
t.is(input3.snippets[0].name, data1.snippets[0].name) t.is(input3.snippets[0].name, data1.snippets[0].name)
t.is(input3.snippets[0].name, jsonData1.snippets[0].name) t.is(input3.snippets[0].name, jsonData1.snippets[0].name)
t.deepEqual(input3.snippets[0].linesHighlighted, data1.snippets[0].linesHighlighted)
t.deepEqual(input3.snippets[0].linesHighlighted, jsonData1.snippets[0].linesHighlighted)
const jsonData2 = CSON.readFileSync(path.join(storagePath, 'notes', data2.key + '.cson')) const jsonData2 = CSON.readFileSync(path.join(storagePath, 'notes', data2.key + '.cson'))
t.is(input4.title, data2.title) t.is(input4.title, data2.title)
@@ -107,6 +116,8 @@ test.serial('Update a note', (t) => {
t.is(input4.content, jsonData2.content) t.is(input4.content, jsonData2.content)
t.is(input4.tags.length, data2.tags.length) t.is(input4.tags.length, data2.tags.length)
t.is(input4.tags.length, jsonData2.tags.length) t.is(input4.tags.length, jsonData2.tags.length)
t.deepEqual(input4.linesHighlighted, data2.linesHighlighted)
t.deepEqual(input4.linesHighlighted, jsonData2.linesHighlighted)
}) })
}) })

161
yarn.lock
View File

@@ -61,7 +61,6 @@
"@enyaxu/markdown-it-anchor@^5.0.2": "@enyaxu/markdown-it-anchor@^5.0.2":
version "5.0.2" version "5.0.2"
resolved "https://registry.yarnpkg.com/@enyaxu/markdown-it-anchor/-/markdown-it-anchor-5.0.2.tgz#d173f7b60b492aabc17dfba864c4d071f5595f72" resolved "https://registry.yarnpkg.com/@enyaxu/markdown-it-anchor/-/markdown-it-anchor-5.0.2.tgz#d173f7b60b492aabc17dfba864c4d071f5595f72"
integrity sha512-HBQ+by3IFHh2i5nw8fzn9qrdA+6uwzre68EzHpBX/WrwgnKrfvckPzdi7MphKp2C617edfpeibucslHDNPYkvQ==
"@ladjs/time-require@^0.1.4": "@ladjs/time-require@^0.1.4":
version "0.1.4" version "0.1.4"
@@ -1690,6 +1689,10 @@ ci-info@^1.0.0:
version "1.1.3" version "1.1.3"
resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.1.3.tgz#710193264bb05c77b8c90d02f5aaf22216a667b2" resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.1.3.tgz#710193264bb05c77b8c90d02f5aaf22216a667b2"
ci-info@^1.5.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.6.0.tgz#2ca20dbb9ceb32d4524a683303313f0304b1e497"
circular-json@^0.3.1: circular-json@^0.3.1:
version "0.3.3" version "0.3.3"
resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.3.tgz#815c99ea84f6809529d2f45791bdf82711352d66" resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.3.tgz#815c99ea84f6809529d2f45791bdf82711352d66"
@@ -2107,6 +2110,14 @@ core-util-is@1.0.2, core-util-is@~1.0.0:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
cosmiconfig@^5.0.6:
version "5.0.6"
resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-5.0.6.tgz#dca6cf680a0bd03589aff684700858c81abeeb39"
dependencies:
is-directory "^0.3.1"
js-yaml "^3.9.0"
parse-json "^4.0.0"
create-error-class@^3.0.0, create-error-class@^3.0.1: create-error-class@^3.0.0, create-error-class@^3.0.1:
version "3.0.2" version "3.0.2"
resolved "https://registry.yarnpkg.com/create-error-class/-/create-error-class-3.0.2.tgz#06be7abef947a3f14a30fd610671d401bca8b7b6" resolved "https://registry.yarnpkg.com/create-error-class/-/create-error-class-3.0.2.tgz#06be7abef947a3f14a30fd610671d401bca8b7b6"
@@ -2749,6 +2760,10 @@ doctrine@^2.0.0, doctrine@^2.0.2:
dependencies: dependencies:
esutils "^2.0.2" esutils "^2.0.2"
dom-helpers@^3.3.1:
version "3.3.1"
resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.3.1.tgz#fc1a4e15ffdf60ddde03a480a9c0fece821dd4a6"
dom-serializer@0: dom-serializer@0:
version "0.1.0" version "0.1.0"
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82"
@@ -3364,6 +3379,18 @@ execa@^0.7.0:
signal-exit "^3.0.0" signal-exit "^3.0.0"
strip-eof "^1.0.0" strip-eof "^1.0.0"
execa@^0.9.0:
version "0.9.0"
resolved "https://registry.yarnpkg.com/execa/-/execa-0.9.0.tgz#adb7ce62cf985071f60580deb4a88b9e34712d01"
dependencies:
cross-spawn "^5.0.1"
get-stream "^3.0.0"
is-stream "^1.1.0"
npm-run-path "^2.0.0"
p-finally "^1.0.0"
signal-exit "^3.0.0"
strip-eof "^1.0.0"
exit-hook@^1.0.0: exit-hook@^1.0.0:
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8" resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8"
@@ -3591,9 +3618,9 @@ filename-reserved-regex@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz#abf73dfab735d045440abfea2d91f389ebbfa229" resolved "https://registry.yarnpkg.com/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz#abf73dfab735d045440abfea2d91f389ebbfa229"
filenamify@^2.0.0: filenamify@^2.1.0:
version "2.0.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/filenamify/-/filenamify-2.0.0.tgz#bd162262c0b6e94bfbcdcf19a3bbb3764f785695" resolved "https://registry.yarnpkg.com/filenamify/-/filenamify-2.1.0.tgz#88faf495fb1b47abfd612300002a16228c677ee9"
dependencies: dependencies:
filename-reserved-regex "^2.0.0" filename-reserved-regex "^2.0.0"
strip-outer "^1.0.0" strip-outer "^1.0.0"
@@ -3670,6 +3697,12 @@ find-up@^2.0.0, find-up@^2.1.0:
dependencies: dependencies:
locate-path "^2.0.0" locate-path "^2.0.0"
find-up@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73"
dependencies:
locate-path "^3.0.0"
findup-sync@~0.1.2: findup-sync@~0.1.2:
version "0.1.3" version "0.1.3"
resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-0.1.3.tgz#7f3e7a97b82392c653bf06589bd85190e93c3683" resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-0.1.3.tgz#7f3e7a97b82392c653bf06589bd85190e93c3683"
@@ -3915,6 +3948,10 @@ get-stdin@^5.0.1:
version "5.0.1" version "5.0.1"
resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-5.0.1.tgz#122e161591e21ff4c52530305693f20e6393a398" resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-5.0.1.tgz#122e161591e21ff4c52530305693f20e6393a398"
get-stdin@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-6.0.0.tgz#9e09bf712b360ab9225e812048f71fde9c89657b"
get-stream@^3.0.0: get-stream@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14"
@@ -4452,6 +4489,21 @@ humanize-plus@^1.8.1:
version "1.8.2" version "1.8.2"
resolved "https://registry.yarnpkg.com/humanize-plus/-/humanize-plus-1.8.2.tgz#a65b34459ad6367adbb3707a82a3c9f916167030" resolved "https://registry.yarnpkg.com/humanize-plus/-/humanize-plus-1.8.2.tgz#a65b34459ad6367adbb3707a82a3c9f916167030"
husky@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/husky/-/husky-1.1.0.tgz#7271e85f5d98b54349788839b720c9a60cd95dba"
dependencies:
cosmiconfig "^5.0.6"
execa "^0.9.0"
find-up "^3.0.0"
get-stdin "^6.0.0"
is-ci "^1.2.1"
pkg-dir "^3.0.0"
please-upgrade-node "^3.1.1"
read-pkg "^4.0.1"
run-node "^1.0.0"
slash "^2.0.0"
i18n-2@^0.7.2: i18n-2@^0.7.2:
version "0.7.2" version "0.7.2"
resolved "https://registry.yarnpkg.com/i18n-2/-/i18n-2-0.7.2.tgz#7efb1a7adc67869adea0688951577464aa793aaf" resolved "https://registry.yarnpkg.com/i18n-2/-/i18n-2-0.7.2.tgz#7efb1a7adc67869adea0688951577464aa793aaf"
@@ -4662,6 +4714,12 @@ is-ci@^1.0.10, is-ci@^1.0.7:
dependencies: dependencies:
ci-info "^1.0.0" ci-info "^1.0.0"
is-ci@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-1.2.1.tgz#e3779c8ee17fccf428488f6e281187f2e632841c"
dependencies:
ci-info "^1.5.0"
is-data-descriptor@^0.1.4: is-data-descriptor@^0.1.4:
version "0.1.4" version "0.1.4"
resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56"
@@ -4694,6 +4752,10 @@ is-descriptor@^1.0.0, is-descriptor@^1.0.2:
is-data-descriptor "^1.0.0" is-data-descriptor "^1.0.0"
kind-of "^6.0.2" kind-of "^6.0.2"
is-directory@^0.3.1:
version "0.3.1"
resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1"
is-dotfile@^1.0.0: is-dotfile@^1.0.0:
version "1.0.3" version "1.0.3"
resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1" resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1"
@@ -5324,6 +5386,10 @@ js-tokens@^3.0.0, js-tokens@^3.0.2:
version "3.0.2" version "3.0.2"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
"js-tokens@^3.0.0 || ^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
js-yaml@^3.10.0, js-yaml@^3.5.1, js-yaml@^3.7.0: js-yaml@^3.10.0, js-yaml@^3.5.1, js-yaml@^3.7.0:
version "3.11.0" version "3.11.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.11.0.tgz#597c1a8bd57152f26d622ce4117851a51f5ebaef" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.11.0.tgz#597c1a8bd57152f26d622ce4117851a51f5ebaef"
@@ -5331,7 +5397,7 @@ js-yaml@^3.10.0, js-yaml@^3.5.1, js-yaml@^3.7.0:
argparse "^1.0.7" argparse "^1.0.7"
esprima "^4.0.0" esprima "^4.0.0"
js-yaml@^3.12.0, js-yaml@^3.8.1: js-yaml@^3.12.0, js-yaml@^3.8.1, js-yaml@^3.9.0:
version "3.12.0" version "3.12.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.12.0.tgz#eaed656ec8344f10f527c6bfa1b6e2244de167d1" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.12.0.tgz#eaed656ec8344f10f527c6bfa1b6e2244de167d1"
dependencies: dependencies:
@@ -5688,6 +5754,13 @@ locate-path@^2.0.0:
p-locate "^2.0.0" p-locate "^2.0.0"
path-exists "^3.0.0" path-exists "^3.0.0"
locate-path@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e"
dependencies:
p-locate "^3.0.0"
path-exists "^3.0.0"
lodash-es@^4.2.1: lodash-es@^4.2.1:
version "4.17.10" version "4.17.10"
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.10.tgz#62cd7104cdf5dd87f235a837f0ede0e8e5117e05" resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.10.tgz#62cd7104cdf5dd87f235a837f0ede0e8e5117e05"
@@ -5805,6 +5878,12 @@ loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3
dependencies: dependencies:
js-tokens "^3.0.0" js-tokens "^3.0.0"
loose-envify@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
dependencies:
js-tokens "^3.0.0 || ^4.0.0"
loud-rejection@^1.0.0, loud-rejection@^1.2.0: loud-rejection@^1.0.0, loud-rejection@^1.2.0:
version "1.6.0" version "1.6.0"
resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f"
@@ -6681,16 +6760,32 @@ p-limit@^1.1.0:
dependencies: dependencies:
p-try "^1.0.0" p-try "^1.0.0"
p-limit@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.0.0.tgz#e624ed54ee8c460a778b3c9f3670496ff8a57aec"
dependencies:
p-try "^2.0.0"
p-locate@^2.0.0: p-locate@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43"
dependencies: dependencies:
p-limit "^1.1.0" p-limit "^1.1.0"
p-locate@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4"
dependencies:
p-limit "^2.0.0"
p-try@^1.0.0: p-try@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3"
p-try@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.0.0.tgz#85080bb87c64688fa47996fe8f7dfbe8211760b1"
package-hash@^1.2.0: package-hash@^1.2.0:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/package-hash/-/package-hash-1.2.0.tgz#003e56cd57b736a6ed6114cc2b81542672770e44" resolved "https://registry.yarnpkg.com/package-hash/-/package-hash-1.2.0.tgz#003e56cd57b736a6ed6114cc2b81542672770e44"
@@ -6886,12 +6981,24 @@ pkg-dir@^2.0.0:
dependencies: dependencies:
find-up "^2.1.0" find-up "^2.1.0"
pkg-dir@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3"
dependencies:
find-up "^3.0.0"
pkg-up@^2.0.0: pkg-up@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-2.0.0.tgz#c819ac728059a461cab1c3889a2be3c49a004d7f" resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-2.0.0.tgz#c819ac728059a461cab1c3889a2be3c49a004d7f"
dependencies: dependencies:
find-up "^2.1.0" find-up "^2.1.0"
please-upgrade-node@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/please-upgrade-node/-/please-upgrade-node-3.1.1.tgz#ed320051dfcc5024fae696712c8288993595e8ac"
dependencies:
semver-compare "^1.0.0"
plist@^2.0.0, plist@^2.1.0: plist@^2.0.0, plist@^2.1.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/plist/-/plist-2.1.0.tgz#57ccdb7a0821df21831217a3cad54e3e146a1025" resolved "https://registry.yarnpkg.com/plist/-/plist-2.1.0.tgz#57ccdb7a0821df21831217a3cad54e3e146a1025"
@@ -7241,6 +7348,13 @@ prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.7, prop-types@^15.5.8,
loose-envify "^1.3.1" loose-envify "^1.3.1"
object-assign "^4.1.1" object-assign "^4.1.1"
prop-types@^15.6.2:
version "15.6.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.2.tgz#05d5ca77b4453e985d60fc7ff8c859094a497102"
dependencies:
loose-envify "^1.3.1"
object-assign "^4.1.1"
proxy-addr@~2.0.3: proxy-addr@~2.0.3:
version "2.0.3" version "2.0.3"
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.3.tgz#355f262505a621646b3130a728eb647e22055341" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.3.tgz#355f262505a621646b3130a728eb647e22055341"
@@ -7420,6 +7534,10 @@ react-dom@^15.0.2:
object-assign "^4.1.0" object-assign "^4.1.0"
prop-types "^15.5.10" prop-types "^15.5.10"
react-image-carousel@^2.0.18:
version "2.0.18"
resolved "https://registry.yarnpkg.com/react-image-carousel/-/react-image-carousel-2.0.18.tgz#5868ea09bd9cca09c4467d3d02695cd4e7792f28"
react-input-autosize@^1.1.0: react-input-autosize@^1.1.0:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/react-input-autosize/-/react-input-autosize-1.2.0.tgz#87241071159f742123897691da6796ec33b57d05" resolved "https://registry.yarnpkg.com/react-input-autosize/-/react-input-autosize-1.2.0.tgz#87241071159f742123897691da6796ec33b57d05"
@@ -7427,6 +7545,10 @@ react-input-autosize@^1.1.0:
create-react-class "^15.5.2" create-react-class "^15.5.2"
prop-types "^15.5.8" prop-types "^15.5.8"
react-lifecycles-compat@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
react-proxy@^1.1.7: react-proxy@^1.1.7:
version "1.1.8" version "1.1.8"
resolved "https://registry.yarnpkg.com/react-proxy/-/react-proxy-1.1.8.tgz#9dbfd9d927528c3aa9f444e4558c37830ab8c26a" resolved "https://registry.yarnpkg.com/react-proxy/-/react-proxy-1.1.8.tgz#9dbfd9d927528c3aa9f444e4558c37830ab8c26a"
@@ -7492,6 +7614,15 @@ react-transform-hmr@^1.0.3:
global "^4.3.0" global "^4.3.0"
react-proxy "^1.1.7" react-proxy "^1.1.7"
react-transition-group@^2.5.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.5.0.tgz#70bca0e3546102c4dc5cf3f5f57f73447cce6874"
dependencies:
dom-helpers "^3.3.1"
loose-envify "^1.4.0"
prop-types "^15.6.2"
react-lifecycles-compat "^3.0.4"
react@^15.5.4: react@^15.5.4:
version "15.6.2" version "15.6.2"
resolved "https://registry.yarnpkg.com/react/-/react-15.6.2.tgz#dba0434ab439cfe82f108f0f511663908179aa72" resolved "https://registry.yarnpkg.com/react/-/react-15.6.2.tgz#dba0434ab439cfe82f108f0f511663908179aa72"
@@ -7545,6 +7676,14 @@ read-pkg@^2.0.0:
normalize-package-data "^2.3.2" normalize-package-data "^2.3.2"
path-type "^2.0.0" path-type "^2.0.0"
read-pkg@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-4.0.1.tgz#963625378f3e1c4d48c85872b5a6ec7d5d093237"
dependencies:
normalize-package-data "^2.3.2"
parse-json "^4.0.0"
pify "^3.0.0"
readable-stream@^1.1.8, readable-stream@~1.1.9: readable-stream@^1.1.8, readable-stream@~1.1.9:
version "1.1.14" version "1.1.14"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9"
@@ -7895,6 +8034,10 @@ run-async@^0.1.0:
dependencies: dependencies:
once "^1.3.0" once "^1.3.0"
run-node@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/run-node/-/run-node-1.0.0.tgz#46b50b946a2aa2d4947ae1d886e9856fd9cabe5e"
run-parallel@^1.1.2: run-parallel@^1.1.2:
version "1.1.9" version "1.1.9"
resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.1.9.tgz#c9dd3a7cf9f4b2c4b6244e173a6ed866e61dd679" resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.1.9.tgz#c9dd3a7cf9f4b2c4b6244e173a6ed866e61dd679"
@@ -7994,6 +8137,10 @@ section-iterator@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/section-iterator/-/section-iterator-2.0.0.tgz#bf444d7afeeb94ad43c39ad2fb26151627ccba2a" resolved "https://registry.yarnpkg.com/section-iterator/-/section-iterator-2.0.0.tgz#bf444d7afeeb94ad43c39ad2fb26151627ccba2a"
semver-compare@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc"
semver-diff@^2.0.0: semver-diff@^2.0.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-2.1.0.tgz#4bbb8437c8d37e4b0cf1a68fd726ec6d645d6d36" resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-2.1.0.tgz#4bbb8437c8d37e4b0cf1a68fd726ec6d645d6d36"
@@ -8153,6 +8300,10 @@ slash@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55"
slash@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44"
slice-ansi@0.0.4: slice-ansi@0.0.4:
version "0.0.4" version "0.0.4"
resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35" resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35"