Skip to content

Commit e77db37

Browse files
Merge branch 'master' into feature/autoBracketMatching
2 parents ed742c7 + 64ca875 commit e77db37

File tree

9 files changed

+213
-73
lines changed

9 files changed

+213
-73
lines changed

browser/components/CodeEditor.js

Lines changed: 135 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,7 @@ import crypto from 'crypto'
1717
import consts from 'browser/lib/consts'
1818
import styles from '../components/CodeEditor.styl'
1919
import fs from 'fs'
20-
21-
const { ipcRenderer, remote } = require('electron')
22-
20+
const { ipcRenderer, remote, clipboard } = require('electron')
2321
import normalizeEditorFontFamily from 'browser/lib/normalizeEditorFontFamily'
2422
const spellcheck = require('browser/lib/spellcheck')
2523
const buildEditorContextMenu = require('browser/lib/contextMenuBuilder')
@@ -35,6 +33,10 @@ const buildCMRulers = (rulers, enableRulers) =>
3533
column: ruler
3634
})) : [])
3735

36+
function translateHotkey (hotkey) {
37+
return hotkey.replace(/\s*\+\s*/g, '-').replace(/Command/g, 'Cmd').replace(/Control/g, 'Ctrl')
38+
}
39+
3840
export default class CodeEditor extends React.Component {
3941
constructor (props) {
4042
super(props)
@@ -69,7 +71,11 @@ export default class CodeEditor extends React.Component {
6971
noteKey
7072
)
7173
}
72-
this.pasteHandler = (editor, e) => this.handlePaste(editor, e)
74+
this.pasteHandler = (editor, e) => {
75+
e.preventDefault()
76+
77+
this.handlePaste(editor, false)
78+
}
7379
this.loadStyleHandler = e => {
7480
this.editor.refresh()
7581
}
@@ -139,40 +145,9 @@ export default class CodeEditor extends React.Component {
139145
}
140146
}
141147

142-
updateTableEditorState () {
143-
const active = this.tableEditor.cursorIsInTable(this.tableEditorOptions)
144-
if (active) {
145-
if (this.extraKeysMode !== 'editor') {
146-
this.extraKeysMode = 'editor'
147-
this.editor.setOption('extraKeys', this.editorKeyMap)
148-
}
149-
} else {
150-
if (this.extraKeysMode !== 'default') {
151-
this.extraKeysMode = 'default'
152-
this.editor.setOption('extraKeys', this.defaultKeyMap)
153-
this.tableEditor.resetSmartCursor()
154-
}
155-
}
156-
}
157-
158-
componentDidMount () {
159-
const { rulers, enableRulers, switchPreview } = this.props
148+
updateDefaultKeyMap () {
149+
const { hotkey } = this.props
160150
const expandSnippet = this.expandSnippet.bind(this)
161-
eventEmitter.on('line:jump', this.scrollToLineHandeler)
162-
163-
const defaultSnippet = [{
164-
id: crypto.randomBytes(16).toString('hex'),
165-
name: 'Dummy text',
166-
prefix: ['lorem', 'ipsum'],
167-
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.'
168-
}]
169-
if (!fs.existsSync(consts.SNIPPET_FILE)) {
170-
fs.writeFileSync(
171-
consts.SNIPPET_FILE,
172-
JSON.stringify(defaultSnippet, null, 4),
173-
'utf8'
174-
)
175-
}
176151

177152
this.defaultKeyMap = CodeMirror.normalizeKeyMap({
178153
Tab: function (cm) {
@@ -224,8 +199,50 @@ export default class CodeEditor extends React.Component {
224199
document.execCommand('copy')
225200
}
226201
return CodeMirror.Pass
202+
},
203+
[translateHotkey(hotkey.pasteSmartly)]: cm => {
204+
this.handlePaste(cm, true)
227205
}
228206
})
207+
}
208+
209+
updateTableEditorState () {
210+
const active = this.tableEditor.cursorIsInTable(this.tableEditorOptions)
211+
if (active) {
212+
if (this.extraKeysMode !== 'editor') {
213+
this.extraKeysMode = 'editor'
214+
this.editor.setOption('extraKeys', this.editorKeyMap)
215+
}
216+
} else {
217+
if (this.extraKeysMode !== 'default') {
218+
this.extraKeysMode = 'default'
219+
this.editor.setOption('extraKeys', this.defaultKeyMap)
220+
this.tableEditor.resetSmartCursor()
221+
}
222+
}
223+
}
224+
225+
componentDidMount () {
226+
const { rulers, enableRulers } = this.props
227+
eventEmitter.on('line:jump', this.scrollToLineHandeler)
228+
229+
const defaultSnippet = [
230+
{
231+
id: crypto.randomBytes(16).toString('hex'),
232+
name: 'Dummy text',
233+
prefix: ['lorem', 'ipsum'],
234+
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.'
235+
}
236+
]
237+
if (!fs.existsSync(consts.SNIPPET_FILE)) {
238+
fs.writeFileSync(
239+
consts.SNIPPET_FILE,
240+
JSON.stringify(defaultSnippet, null, 4),
241+
'utf8'
242+
)
243+
}
244+
245+
this.updateDefaultKeyMap()
229246

230247
this.value = this.props.value
231248
this.editor = CodeMirror(this.refs.root, {
@@ -258,7 +275,7 @@ export default class CodeEditor extends React.Component {
258275
this.editor.on('blur', this.blurHandler)
259276
this.editor.on('change', this.changeHandler)
260277
this.editor.on('paste', this.pasteHandler)
261-
if (switchPreview !== 'RIGHTCLICK') {
278+
if (this.props.switchPreview !== 'RIGHTCLICK') {
262279
this.editor.on('contextmenu', this.contextMenuHandler)
263280
}
264281
eventEmitter.on('top:search', this.searchHandler)
@@ -587,6 +604,14 @@ export default class CodeEditor extends React.Component {
587604
this.editor.setOption('extraKeys', this.defaultKeyMap)
588605
}
589606

607+
if (prevProps.hotkey !== this.props.hotkey) {
608+
this.updateDefaultKeyMap()
609+
610+
if (this.extraKeysMode === 'default') {
611+
this.editor.setOption('extraKeys', this.defaultKeyMap)
612+
}
613+
}
614+
590615
if (this.state.clientWidth !== this.refs.root.clientWidth) {
591616
this.setState({
592617
clientWidth: this.refs.root.clientWidth
@@ -678,18 +703,15 @@ export default class CodeEditor extends React.Component {
678703
this.editor.replaceSelection(imageMd)
679704
}
680705

681-
handlePaste (editor, e) {
682-
const clipboardData = e.clipboardData
683-
const {
684-
storageKey,
685-
noteKey
686-
} = this.props
687-
const dataTransferItem = clipboardData.items[0]
688-
const pastedTxt = clipboardData.getData('text')
706+
707+
handlePaste (editor, forceSmartPaste) {
708+
const { storageKey, noteKey, fetchUrlTitle, enableSmartPaste } = this.props
709+
689710
const isURL = str => {
690711
const matcher = /^(?:\w+:)?\/\/([^\s\.]+\.\S{2}|localhost[\:?\d]*)\S*$/
691712
return matcher.test(str)
692713
}
714+
693715
const isInLinkTag = editor => {
694716
const startCursor = editor.getCursor('start')
695717
const prevChar = editor.getRange({
@@ -710,30 +732,73 @@ export default class CodeEditor extends React.Component {
710732
return prevChar === '](' && nextChar === ')'
711733
}
712734

713-
const pastedHtml = clipboardData.getData('text/html')
714-
if (pastedHtml !== '') {
715-
this.handlePasteHtml(e, editor, pastedHtml)
716-
} else if (dataTransferItem.type.match('image')) {
717-
attachmentManagement.handlePastImageEvent(
718-
this,
719-
storageKey,
720-
noteKey,
721-
dataTransferItem
722-
)
723-
} else if (
724-
this.props.fetchUrlTitle &&
725-
isURL(pastedTxt) &&
726-
!isInLinkTag(editor)
727-
) {
728-
this.handlePasteUrl(e, editor, pastedTxt)
735+
const isInFencedCodeBlock = editor => {
736+
const cursor = editor.getCursor()
737+
738+
let token = editor.getTokenAt(cursor)
739+
if (token.state.fencedState) {
740+
return true
741+
}
742+
743+
let line = line = cursor.line - 1
744+
while (line >= 0) {
745+
token = editor.getTokenAt({
746+
ch: 3,
747+
line
748+
})
749+
750+
if (token.start === token.end) {
751+
--line
752+
} else if (token.type === 'comment') {
753+
if (line > 0) {
754+
token = editor.getTokenAt({
755+
ch: 3,
756+
line: line - 1
757+
})
758+
759+
return token.type !== 'comment'
760+
} else {
761+
return true
762+
}
763+
} else {
764+
return false
765+
}
766+
}
767+
768+
return false
729769
}
730-
if (attachmentManagement.isAttachmentLink(pastedTxt)) {
770+
771+
const pastedTxt = clipboard.readText()
772+
773+
if (isInFencedCodeBlock(editor)) {
774+
this.handlePasteText(editor, pastedTxt)
775+
} else if (fetchUrlTitle && isURL(pastedTxt) && !isInLinkTag(editor)) {
776+
this.handlePasteUrl(editor, pastedTxt)
777+
} else if (enableSmartPaste || forceSmartPaste) {
778+
const image = clipboard.readImage()
779+
if (!image.isEmpty()) {
780+
attachmentManagement.handlePastNativeImage(
781+
this,
782+
storageKey,
783+
noteKey,
784+
image
785+
)
786+
} else {
787+
const pastedHtml = clipboard.readHTML()
788+
if (pastedHtml.length > 0) {
789+
this.handlePasteHtml(editor, pastedHtml)
790+
} else {
791+
this.handlePasteText(editor, pastedTxt)
792+
}
793+
}
794+
} else if (attachmentManagement.isAttachmentLink(pastedTxt)) {
731795
attachmentManagement
732796
.handleAttachmentLinkPaste(storageKey, noteKey, pastedTxt)
733797
.then(modifiedText => {
734798
this.editor.replaceSelection(modifiedText)
735799
})
736-
e.preventDefault()
800+
} else {
801+
this.handlePasteText(editor, pastedTxt)
737802
}
738803
}
739804

@@ -743,8 +808,7 @@ export default class CodeEditor extends React.Component {
743808
}
744809
}
745810

746-
handlePasteUrl (e, editor, pastedTxt) {
747-
e.preventDefault()
811+
handlePasteUrl (editor, pastedTxt) {
748812
const taggedUrl = `<${pastedTxt}>`
749813
editor.replaceSelection(taggedUrl)
750814

@@ -783,12 +847,15 @@ export default class CodeEditor extends React.Component {
783847
})
784848
}
785849

786-
handlePasteHtml (e, editor, pastedHtml) {
787-
e.preventDefault()
850+
handlePasteHtml (editor, pastedHtml) {
788851
const markdown = this.turndownService.turndown(pastedHtml)
789852
editor.replaceSelection(markdown)
790853
}
791854

855+
handlePasteText (editor, pastedTxt) {
856+
editor.replaceSelection(pastedTxt)
857+
}
858+
792859
mapNormalResponse (response, pastedTxt) {
793860
return this.decodeResponse(response).then(body => {
794861
return new Promise((resolve, reject) => {

browser/components/MarkdownEditor.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,8 @@ class MarkdownEditor extends React.Component {
281281
onChange={(e) => this.handleChange(e)}
282282
onBlur={(e) => this.handleBlur(e)}
283283
spellCheck={config.editor.spellcheck}
284+
enableSmartPaste={config.editor.enableSmartPaste}
285+
hotkey={config.hotkey}
284286
switchPreview={config.editor.switchPreview}
285287
/>
286288
<MarkdownPreview styleName={this.state.status === 'PREVIEW'

browser/components/MarkdownSplitEditor.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,8 @@ class MarkdownSplitEditor extends React.Component {
175175
onChange={this.handleOnChange.bind(this)}
176176
onScroll={this.handleScroll.bind(this)}
177177
spellCheck={config.editor.spellcheck}
178+
enableSmartPaste={config.editor.enableSmartPaste}
179+
hotkey={config.hotkey}
178180
switchPreview={config.editor.switchPreview}
179181
/>
180182
<div styleName='slider' style={{left: this.state.codeEditorWidthInPercent + '%'}} onMouseDown={e => this.handleMouseDown(e)} >

browser/main/Detail/SnippetNoteDetail.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -727,6 +727,8 @@ class SnippetNoteDetail extends React.Component {
727727
enableTableEditor={config.editor.enableTableEditor}
728728
onChange={(e) => this.handleCodeChange(index)(e)}
729729
ref={'code-' + index}
730+
enableSmartPaste={config.editor.enableSmartPaste}
731+
hotkey={config.hotkey}
730732
/>
731733
}
732734
</div>

browser/main/lib/ConfigManager.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ export const DEFAULT_CONFIG = {
2525
hotkey: {
2626
toggleMain: OSX ? 'Command + Alt + L' : 'Super + Alt + E',
2727
toggleMode: OSX ? 'Command + Alt + M' : 'Ctrl + M',
28-
deleteNote: OSX ? 'Command + Shift + Backspace' : 'Ctrl + Shift + Backspace'
28+
deleteNote: OSX ? 'Command + Shift + Backspace' : 'Ctrl + Shift + Backspace',
29+
pasteSmartly: OSX ? 'Command + Shift + V' : 'Ctrl + Shift + V'
2930
},
3031
ui: {
3132
language: 'en',
@@ -55,7 +56,8 @@ export const DEFAULT_CONFIG = {
5556
enableTableEditor: false,
5657
enableFrontMatterTitle: true,
5758
frontMatterTitleField: 'title',
58-
spellcheck: false
59+
spellcheck: false,
60+
enableSmartPaste: false
5961
},
6062
preview: {
6163
fontSize: '14',

browser/main/lib/dataApi/attachmentManagement.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,44 @@ function handlePastImageEvent (codeEditor, storageKey, noteKey, dataTransferItem
316316
reader.readAsDataURL(blob)
317317
}
318318

319+
/**
320+
* @description Creates a new file in the storage folder belonging to the current note and inserts the correct markdown code
321+
* @param {CodeEditor} codeEditor Markdown editor. Its insertAttachmentMd() method will be called to include the markdown code
322+
* @param {String} storageKey Key of the current storage
323+
* @param {String} noteKey Key of the current note
324+
* @param {NativeImage} image The native image
325+
*/
326+
function handlePastNativeImage (codeEditor, storageKey, noteKey, image) {
327+
if (!codeEditor) {
328+
throw new Error('codeEditor has to be given')
329+
}
330+
if (!storageKey) {
331+
throw new Error('storageKey has to be given')
332+
}
333+
334+
if (!noteKey) {
335+
throw new Error('noteKey has to be given')
336+
}
337+
if (!image) {
338+
throw new Error('image has to be given')
339+
}
340+
341+
const targetStorage = findStorage.findStorage(storageKey)
342+
const destinationDir = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey)
343+
344+
createAttachmentDestinationFolder(targetStorage.path, noteKey)
345+
346+
const imageName = `${uniqueSlug()}.png`
347+
const imagePath = path.join(destinationDir, imageName)
348+
349+
const binaryData = image.toPNG()
350+
fs.writeFileSync(imagePath, binaryData, 'binary')
351+
352+
const imageReferencePath = path.join(STORAGE_FOLDER_PLACEHOLDER, noteKey, imageName)
353+
const imageMd = generateAttachmentMarkdown(imageName, imageReferencePath, true)
354+
codeEditor.insertAttachmentMd(imageMd)
355+
}
356+
319357
/**
320358
* @description Returns all attachment paths of the given markdown
321359
* @param {String} markdownContent content in which the attachment paths should be found
@@ -539,6 +577,7 @@ module.exports = {
539577
generateAttachmentMarkdown,
540578
handleAttachmentDrop,
541579
handlePastImageEvent,
580+
handlePastNativeImage,
542581
getAttachmentsInMarkdownContent,
543582
getAbsolutePathsOfAttachmentsInContent,
544583
removeStorageAndNoteReferences,

0 commit comments

Comments
 (0)