Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes #1825 Refactoring of the attachment/image management #1852

Merged
merged 2 commits into from
Apr 26, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 9 additions & 42 deletions browser/components/CodeEditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,10 @@ import React from 'react'
import _ from 'lodash'
import CodeMirror from 'codemirror'
import 'codemirror-mode-elixir'
import path from 'path'
import copyImage from 'browser/main/lib/dataApi/copyImage'
import { findStorage } from 'browser/lib/findStorage'
import fs from 'fs'
import attachmentManagement from 'browser/main/lib/dataApi/attachmentManagement'
import eventEmitter from 'browser/main/lib/eventEmitter'
import iconv from 'iconv-lite'

const { ipcRenderer } = require('electron')

CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js'
Expand Down Expand Up @@ -275,23 +273,13 @@ export default class CodeEditor extends React.Component {
this.editor.setCursor(cursor)
}

handleDropImage (e) {
e.preventDefault()
const ValidImageTypes = ['image/gif', 'image/jpeg', 'image/png']

const file = e.dataTransfer.files[0]
const filePath = file.path
const filename = path.basename(filePath)
const fileType = file['type']

copyImage(filePath, this.props.storageKey).then((imagePath) => {
var showPreview = ValidImageTypes.indexOf(fileType) > 0
const imageMd = `${showPreview ? '!' : ''}[${filename}](${path.join('/:storage', imagePath)})`
this.insertImageMd(imageMd)
})
handleDropImage (dropEvent) {
dropEvent.preventDefault()
const {storageKey, noteKey} = this.props
attachmentManagement.handleAttachmentDrop(this, storageKey, noteKey, dropEvent)
}

insertImageMd (imageMd) {
insertAttachmentMd (imageMd) {
this.editor.replaceSelection(imageMd)
}

Expand All @@ -317,29 +305,8 @@ export default class CodeEditor extends React.Component {
return prevChar === '](' && nextChar === ')'
}
if (dataTransferItem.type.match('image')) {
const blob = dataTransferItem.getAsFile()
const reader = new FileReader()
let base64data

reader.readAsDataURL(blob)
reader.onloadend = () => {
base64data = reader.result.replace(/^data:image\/png;base64,/, '')
base64data += base64data.replace('+', ' ')
const binaryData = new Buffer(base64data, 'base64').toString('binary')
const imageName = Math.random().toString(36).slice(-16)
const storagePath = findStorage(this.props.storageKey).path
const imageDir = path.join(storagePath, 'images')
if (!fs.existsSync(imageDir)) fs.mkdirSync(imageDir)
const imagePath = path.join(imageDir, `${imageName}.png`)
fs.writeFile(imagePath, binaryData, 'binary', (error) => {
if (error) {
throw error
} else {
const imageMd = `![${imageName}](${path.join('/:storage', `${imageName}.png`)})`
this.insertImageMd(imageMd)
}
})
}
const {storageKey, noteKey} = this.props
attachmentManagement.handlePastImageEvent(this, storageKey, noteKey, dataTransferItem)
} else if (this.props.fetchUrlTitle && isURL(pastedTxt) && !isInLinkTag(editor)) {
this.handlePasteUrl(e, editor, pastedTxt)
}
Expand Down
39 changes: 20 additions & 19 deletions browser/components/MarkdownEditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import styles from './MarkdownEditor.styl'
import CodeEditor from 'browser/components/CodeEditor'
import MarkdownPreview from 'browser/components/MarkdownPreview'
import eventEmitter from 'browser/main/lib/eventEmitter'
import {findStorage} from 'browser/lib/findStorage'
import { findStorage } from 'browser/lib/findStorage'

class MarkdownEditor extends React.Component {
constructor (props) {
Expand Down Expand Up @@ -223,7 +223,7 @@ class MarkdownEditor extends React.Component {
}

render () {
const { className, value, config, storageKey } = this.props
const {className, value, config, storageKey, noteKey} = this.props

let editorFontSize = parseInt(config.editor.fontSize, 10)
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
Expand All @@ -249,23 +249,24 @@ class MarkdownEditor extends React.Component {
? 'codeEditor'
: 'codeEditor--hide'
}
ref='code'
mode='GitHub Flavored Markdown'
value={value}
theme={config.editor.theme}
keyMap={config.editor.keyMap}
fontFamily={config.editor.fontFamily}
fontSize={editorFontSize}
indentType={config.editor.indentType}
indentSize={editorIndentSize}
enableRulers={config.editor.enableRulers}
rulers={config.editor.rulers}
displayLineNumbers={config.editor.displayLineNumbers}
scrollPastEnd={config.editor.scrollPastEnd}
storageKey={storageKey}
fetchUrlTitle={config.editor.fetchUrlTitle}
onChange={(e) => this.handleChange(e)}
onBlur={(e) => this.handleBlur(e)}
ref='code'
mode='GitHub Flavored Markdown'
value={value}
theme={config.editor.theme}
keyMap={config.editor.keyMap}
fontFamily={config.editor.fontFamily}
fontSize={editorFontSize}
indentType={config.editor.indentType}
indentSize={editorIndentSize}
enableRulers={config.editor.enableRulers}
rulers={config.editor.rulers}
displayLineNumbers={config.editor.displayLineNumbers}
scrollPastEnd={config.editor.scrollPastEnd}
storageKey={storageKey}
noteKey={noteKey}
fetchUrlTitle={config.editor.fetchUrlTitle}
onChange={(e) => this.handleChange(e)}
onBlur={(e) => this.handleBlur(e)}
/>
<MarkdownPreview styleName={this.state.status === 'PREVIEW'
? 'preview'
Expand Down
16 changes: 5 additions & 11 deletions browser/components/MarkdownPreview.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ import htmlTextHelper from 'browser/lib/htmlTextHelper'
import copy from 'copy-to-clipboard'
import mdurl from 'mdurl'
import exportNote from 'browser/main/lib/dataApi/exportNote'
import {escapeHtmlCharacters} from 'browser/lib/utils'
import { escapeHtmlCharacters } from 'browser/lib/utils'

const { remote } = require('electron')
const attachmentManagement = require('../main/lib/dataApi/attachmentManagement')

const { app } = remote
const path = require('path')
const dialog = remote.dialog
Expand Down Expand Up @@ -391,13 +393,11 @@ export default class MarkdownPreview extends React.Component {
value = value.replace(codeBlock, htmlTextHelper.encodeEntities(codeBlock))
})
}
this.refs.root.contentWindow.document.body.innerHTML = this.markdown.render(value)
let renderedHTML = this.markdown.render(value)
this.refs.root.contentWindow.document.body.innerHTML = attachmentManagement.fixLocalURLS(renderedHTML, storagePath)

_.forEach(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => {
this.fixDecodedURI(el)
el.href = this.markdown.normalizeLinkText(el.href)
if (!/\/:storage/.test(el.href)) return
el.href = `file:///${this.markdown.normalizeLinkText(path.join(storagePath, 'images', path.basename(el.href)))}`
el.addEventListener('click', this.anchorClickHandler)
})

Expand All @@ -409,12 +409,6 @@ export default class MarkdownPreview extends React.Component {
el.addEventListener('click', this.linkClickHandler)
})

_.forEach(this.refs.root.contentWindow.document.querySelectorAll('img'), (el) => {
el.src = this.markdown.normalizeLinkText(el.src)
if (!/\/:storage/.test(el.src)) return
el.src = `file:///${this.markdown.normalizeLinkText(path.join(storagePath, 'images', path.basename(el.src)))}`
})

codeBlockTheme = consts.THEMES.some((_theme) => _theme === codeBlockTheme)
? codeBlockTheme
: 'default'
Expand Down
3 changes: 2 additions & 1 deletion browser/components/MarkdownSplitEditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ class MarkdownSplitEditor extends React.Component {
}

render () {
const { config, value, storageKey } = this.props
const {config, value, storageKey, noteKey} = this.props
const storage = findStorage(storageKey)
let editorFontSize = parseInt(config.editor.fontSize, 10)
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
Expand All @@ -115,6 +115,7 @@ class MarkdownSplitEditor extends React.Component {
scrollPastEnd={config.editor.scrollPastEnd}
fetchUrlTitle={config.editor.fetchUrlTitle}
storageKey={storageKey}
noteKey={noteKey}
onChange={this.handleOnChange.bind(this)}
onScroll={this.handleScroll.bind(this)}
/>
Expand Down
6 changes: 1 addition & 5 deletions browser/lib/markdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import math from '@rokt33r/markdown-it-math'
import _ from 'lodash'
import ConfigManager from 'browser/main/lib/ConfigManager'
import katex from 'katex'
import {lastFindInArray} from './utils'
import { lastFindInArray } from './utils'

function createGutter (str, firstLineNumber) {
if (Number.isNaN(firstLineNumber)) firstLineNumber = 1
Expand Down Expand Up @@ -234,10 +234,6 @@ class Markdown {
if (!_.isString(content)) content = ''
return this.md.render(content)
}

normalizeLinkText (linkText) {
return this.md.normalizeLinkText(linkText)
}
}

export default Markdown
Expand Down
2 changes: 2 additions & 0 deletions browser/main/Detail/MarkdownNoteDetail.js
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,7 @@ class MarkdownNoteDetail extends React.Component {
config={config}
value={note.content}
storageKey={note.storage}
noteKey={note.key}
onChange={this.handleUpdateContent.bind(this)}
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
/>
Expand All @@ -298,6 +299,7 @@ class MarkdownNoteDetail extends React.Component {
config={config}
value={note.content}
storageKey={note.storage}
noteKey={note.key}
onChange={this.handleUpdateContent.bind(this)}
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
/>
Expand Down
164 changes: 164 additions & 0 deletions browser/main/lib/dataApi/attachmentManagement.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
const uniqueSlug = require('unique-slug')
const fs = require('fs')
const path = require('path')
const findStorage = require('browser/lib/findStorage')
const mdurl = require('mdurl')

const STORAGE_FOLDER_PLACEHOLDER = ':storage'
const DESTINATION_FOLDER = 'attachments'

/**
* @description
* Copies a copy of an attachment to the storage folder specified by the given key and return the generated attachment name.
* Renames the file to match a unique file name.
*
* @param {String} sourceFilePath The source path of the attachment to be copied
* @param {String} storageKey Storage key of the destination storage
* @param {String} noteKey Key of the current note. Will be used as subfolder in :storage
* @param {boolean} useRandomName determines whether a random filename for the new file is used. If false the source file name is used
* @return {Promise<String>} name (inclusive extension) of the generated file
*/
function copyAttachment (sourceFilePath, storageKey, noteKey, useRandomName = true) {
return new Promise((resolve, reject) => {
if (!sourceFilePath) {
reject('sourceFilePath has to be given')
}

if (!storageKey) {
reject('storageKey has to be given')
}

if (!noteKey) {
reject('noteKey has to be given')
}

try {
if (!fs.existsSync(sourceFilePath)) {
reject('source file does not exist')
}

const targetStorage = findStorage.findStorage(storageKey)

const inputFile = fs.createReadStream(sourceFilePath)
let destinationName
if (useRandomName) {
destinationName = `${uniqueSlug()}${path.extname(sourceFilePath)}`
} else {
destinationName = path.basename(sourceFilePath)
}
const destinationDir = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey)
createAttachmentDestinationFolder(targetStorage.path, noteKey)
const outputFile = fs.createWriteStream(path.join(destinationDir, destinationName))
inputFile.pipe(outputFile)
resolve(destinationName)
} catch (e) {
return reject(e)
}
})
}

function createAttachmentDestinationFolder (destinationStoragePath, noteKey) {
let destinationDir = path.join(destinationStoragePath, DESTINATION_FOLDER)
if (!fs.existsSync(destinationDir)) {
fs.mkdirSync(destinationDir)
}
destinationDir = path.join(destinationStoragePath, DESTINATION_FOLDER, noteKey)
if (!fs.existsSync(destinationDir)) {
fs.mkdirSync(destinationDir)
}
}

/**
* @description Fixes the URLs embedded in the generated HTML so that they again refer actual local files.
* @param {String} renderedHTML HTML in that the links should be fixed
* @param {String} storagePath Path of the current storage
* @returns {String} postprocessed HTML in which all :storage references are mapped to the actual paths.
*/
function fixLocalURLS (renderedHTML, storagePath) {
return renderedHTML.replace(new RegExp(mdurl.encode(path.sep), 'g'), path.sep).replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER, 'g'), 'file:///' + path.join(storagePath, DESTINATION_FOLDER))
}

/**
* @description Generates the markdown code for a given attachment
* @param {String} fileName Name of the attachment
* @param {String} path Path of the attachment
* @param {Boolean} showPreview Indicator whether the generated markdown should show a preview of the image. Note that at the moment only previews for images are supported
* @returns {String} Generated markdown code
*/
function generateAttachmentMarkdown (fileName, path, showPreview) {
return `${showPreview ? '!' : ''}[${fileName}](${path})`
}

/**
* @description Handles the drop-event of a file. Includes the necessary markdown code and copies the file to the corresponding storage folder.
* The method calls {CodeEditor#insertAttachmentMd()} to include the generated markdown at the needed place!
* @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 {Event} dropEvent DropEvent
*/
function handleAttachmentDrop (codeEditor, storageKey, noteKey, dropEvent) {
const file = dropEvent.dataTransfer.files[0]
const filePath = file.path
const originalFileName = path.basename(filePath)
const fileType = file['type']

copyAttachment(filePath, storageKey, noteKey).then((fileName) => {
let showPreview = fileType.startsWith('image')
let imageMd = generateAttachmentMarkdown(originalFileName, path.join(STORAGE_FOLDER_PLACEHOLDER, noteKey, fileName), showPreview)
codeEditor.insertAttachmentMd(imageMd)
})
}

/**
* @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 {DataTransferItem} dataTransferItem Part of the past-event
*/
function handlePastImageEvent (codeEditor, storageKey, noteKey, dataTransferItem) {
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 (!dataTransferItem) {
throw new Error('dataTransferItem has to be given')
}

const blob = dataTransferItem.getAsFile()
const reader = new FileReader()
let base64data
const targetStorage = findStorage.findStorage(storageKey)
const destinationDir = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey)
createAttachmentDestinationFolder(targetStorage.path, noteKey)

let imageName = `${uniqueSlug()}.png`
const imagePath = path.join(destinationDir, imageName)

reader.onloadend = function () {
base64data = reader.result.replace(/^data:image\/png;base64,/, '')
base64data += base64data.replace('+', ' ')
const binaryData = new Buffer(base64data, 'base64').toString('binary')
fs.writeFile(imagePath, binaryData, 'binary')
let imageMd = generateAttachmentMarkdown(imageName, imagePath, true)
codeEditor.insertAttachmentMd(imageMd)
}
reader.readAsDataURL(blob)
}

module.exports = {
copyAttachment,
fixLocalURLS,
generateAttachmentMarkdown,
handleAttachmentDrop,
handlePastImageEvent,
STORAGE_FOLDER_PLACEHOLDER,
DESTINATION_FOLDER
}
Loading