Add support for relative image directory (#2439)

This commit is contained in:
Felix Häusler 2020-12-23 23:24:27 +01:00 committed by GitHub
parent 38935cc40c
commit 2b53a414f9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 149 additions and 27 deletions

28
docs/IMAGES.md Normal file
View File

@ -0,0 +1,28 @@
# Image support
Mark Text can automatically copy your images into a specified directory or handle images from clipboard.
### Upload to cloud using selected uploader
Please see [here](IMAGE_UPLOADER_CONFIGRATION.md) for more information.
### Move to designated local folder
All images are automatically copied into the specified local directory that may be relative.
**Prefer relative assets folder:**
When this option is enabled, all images are copied relative to the opened file or the root directory when a project is opened. You can specify the path via the *relative image folder name* text box. The local resource directory is used if the file is not saved.
NB: The assets directory name must be a valid path name and Mark Text need write access to the directory.
Examples for relative paths:
- `assets`
- `../assets`
- `.`: current file directory
- `assets/123`
### Keep original location
Mark Text only saves images from clipboard into the specified local directory.

View File

@ -355,6 +355,16 @@
],
"default": "path"
},
"imagePreferRelativeDirectory": {
"description": "Image--Whether to prefer the relative image directory.",
"type": "boolean",
"default": false
},
"imageRelativeDirectoryName": {
"description": "Image--The relative image folder name.",
"type": "string",
"default": "assets"
},
"sideBarVisibility": {
"description": "View--Whether the side bar is visible.",
"type": "boolean",

View File

@ -74,9 +74,11 @@
<script>
import { shell } from 'electron'
import path from 'path'
import log from 'electron-log'
import { mapState } from 'vuex'
// import ViewImage from 'view-image'
import { isChildOfDirectory } from 'common/filesystem/paths'
import Muya from 'muya/lib'
import TablePicker from 'muya/lib/ui/tablePicker'
import QuickInsert from 'muya/lib/ui/quickInsert'
@ -99,7 +101,7 @@ import notice from '@/services/notification'
import Printer from '@/services/printService'
import { isOsSpellcheckerSupported, offsetToWordCursor, validateLineCursor, SpellChecker } from '@/spellchecker'
import { delay, isOsx, animatedScrollTo } from '@/util'
import { moveImageToFolder, uploadImage } from '@/util/fileSystem'
import { moveImageToFolder, moveToRelativeFolder, uploadImage } from '@/util/fileSystem'
import { guessClipboardFilePath } from '@/util/clipboard'
import { getCssForOptions, getHtmlToc } from '@/util/pdf'
import { addCommonStyle, setEditorWidth } from '@/util/theme'
@ -154,6 +156,8 @@ export default {
autoCheck: state => state.preferences.autoCheck,
editorLineWidth: state => state.preferences.editorLineWidth,
imageInsertAction: state => state.preferences.imageInsertAction,
imagePreferRelativeDirectory: state => state.preferences.imagePreferRelativeDirectory,
imageRelativeDirectoryName: state => state.preferences.imageRelativeDirectoryName,
imageFolderPath: state => state.preferences.imageFolderPath,
theme: state => state.preferences.theme,
sequenceTheme: state => state.preferences.sequenceTheme,
@ -165,6 +169,7 @@ export default {
spellcheckerLanguage: state => state.preferences.spellcheckerLanguage,
currentFile: state => state.editor.currentFile,
projectTree: state => state.project.projectTree,
// edit modes
typewriter: state => state.preferences.typewriter,
@ -768,9 +773,26 @@ export default {
},
async imageAction (image, id, alt = '') {
const { imageInsertAction, imageFolderPath, preferences } = this
const {
imageInsertAction,
imageFolderPath,
imagePreferRelativeDirectory,
imageRelativeDirectoryName,
preferences
} = this
const { pathname } = this.currentFile
let result
// Figure out the current working directory.
let cwd = pathname ? path.dirname(pathname) : null
if (pathname && this.projectTree) {
const { pathname: rootPath } = this.projectTree
if (rootPath && isChildOfDirectory(rootPath, pathname)) {
// Save assets relative to root directory.
cwd = rootPath
}
}
let result = ''
switch (imageInsertAction) {
case 'upload': {
try {
@ -787,6 +809,9 @@ export default {
}
case 'folder': {
result = await moveImageToFolder(pathname, image, imageFolderPath)
if (cwd && imagePreferRelativeDirectory) {
result = moveToRelativeFolder(cwd, result, imageRelativeDirectoryName)
}
break
}
case 'path': {
@ -795,6 +820,11 @@ export default {
} else {
// Move image to image folder if it's Blob object.
result = await moveImageToFolder(pathname, image, imageFolderPath)
// Respect user preferences if file exist on disk.
if (cwd && imagePreferRelativeDirectory) {
result = moveToRelativeFolder(cwd, result, imageRelativeDirectoryName)
}
}
break
}

View File

@ -2,8 +2,8 @@
<div class="pref-image">
<h4>Image</h4>
<section class="image-ctrl">
<div>Default action after image is inserted from local folder
<el-tooltip class='item' effect='dark' content='Mark Text can not get image path from paste event on Linux.' placement='top-start'>
<div>Default action after image is inserted from local folder or clipboard
<el-tooltip class='item' effect='dark' content='Clipboard handling is only fully supported on macOS and Windows.' placement='top-start'>
<i class="el-icon-info"></i>
</el-tooltip>
</div>
@ -21,23 +21,45 @@
<el-button size="mini" @click="modifyImageFolderPath">Modify</el-button>
<el-button size="mini" @click="openImageFolder">Open Folder</el-button>
</div>
<bool
description="Prefer relative assets folder"
more="https://github.com/marktext/marktext/blob/develop/docs/IMAGES.md"
:bool="imagePreferRelativeDirectory"
:onChange="value => onSelectChange('imagePreferRelativeDirectory', value)"
></bool>
<text-box
description="Relative image folder name"
:input="imageRelativeDirectoryName"
:regexValidator="/^(?:$|(?![a-zA-Z]:)[^\/\\].*$)/"
:defaultValue="relativeDirectoryNamePlaceholder"
:onChange="value => onSelectChange('imageRelativeDirectoryName', value)"
></text-box>
</section>
</div>
</template>
<script>
import Separator from '../common/separator'
import { mapState } from 'vuex'
import { shell } from 'electron'
import Bool from '../common/bool'
import Separator from '../common/separator'
import TextBox from '../common/textBox'
export default {
components: {
Separator
Bool,
Separator,
TextBox
},
data () {
return {
}
},
computed: {
...mapState({
imagePreferRelativeDirectory: state => state.preferences.imagePreferRelativeDirectory,
imageRelativeDirectoryName: state => state.preferences.imageRelativeDirectoryName
}),
imageInsertAction: {
get: function () {
return this.$store.state.preferences.imageInsertAction
@ -51,6 +73,11 @@ export default {
get: function () {
return this.$store.state.preferences.imageFolderPath
}
},
relativeDirectoryNamePlaceholder: {
get: function () {
return this.$store.state.preferences.imageRelativeDirectoryName || 'assets'
}
}
},
methods: {
@ -59,6 +86,9 @@ export default {
},
modifyImageFolderPath () {
return this.$store.dispatch('SET_IMAGE_FOLDER_PATH')
},
onSelectChange (type, value) {
this.$store.dispatch('SET_SINGLE_PREFERENCE', { type, value })
}
}
}

View File

@ -35,6 +35,8 @@ const state = {
textDirection: 'ltr',
hideQuickInsertHint: false,
imageInsertAction: 'folder',
imagePreferRelativeDirectory: false,
imageRelativeDirectoryName: 'assets',
hideLinkPopup: false,
autoCheck: false,

View File

@ -1,29 +1,59 @@
import path from 'path'
import crypto from 'crypto'
import { clipboard } from 'electron'
import fse from 'fs-extra'
import fs from 'fs-extra'
import dayjs from 'dayjs'
import Octokit from '@octokit/rest'
import { ensureDirSync } from 'common/filesystem'
import { isImageFile } from 'common/filesystem/paths'
import { dataURItoBlob, getContentHash } from './index'
import { dataURItoBlob } from './index'
import axios from '../axios'
export const create = (pathname, type) => {
if (type === 'directory') {
return fse.ensureDir(pathname)
return fs.ensureDir(pathname)
} else {
return fse.outputFile(pathname, '')
return fs.outputFile(pathname, '')
}
}
export const paste = ({ src, dest, type }) => {
return type === 'cut' ? fse.move(src, dest) : fse.copy(src, dest)
return type === 'cut' ? fs.move(src, dest) : fs.copy(src, dest)
}
export const rename = (src, dest) => {
return fse.move(src, dest)
return fs.move(src, dest)
}
export const getHash = (content, encoding, type) => {
return crypto.createHash(type).update(content, encoding).digest('hex')
}
export const getContentHash = content => {
return getHash(content, 'utf8', 'sha1')
}
export const moveToRelativeFolder = async (cwd, imagePath, relativeName) => {
if (!relativeName) {
// Use fallback name according settings description
relativeName = 'assets'
} else if (path.isAbsolute(relativeName)) {
throw new Error('Invalid relative directory name')
}
// Path combination:
// - markdown file directory + relative directory name or
// - root directory + relative directory name
const absPath = path.resolve(cwd, relativeName)
ensureDirSync(absPath)
const dstPath = path.resolve(absPath, path.basename(imagePath))
await fs.move(imagePath, dstPath, { overwrite: true })
return dstPath
}
export const moveImageToFolder = async (pathname, image, dir) => {
ensureDirSync(dir)
const isPath = typeof image === 'string'
if (isPath) {
const dirname = path.dirname(pathname)
@ -39,7 +69,7 @@ export const moveImageToFolder = async (pathname, image, dir) => {
const hash = getContentHash(imagePath)
// To avoid name conflict.
const hashFilePath = path.join(dir, `${hash}${extname}`)
await fse.copy(imagePath, hashFilePath)
await fs.copy(imagePath, hashFilePath)
return hashFilePath
} else {
return Promise.resolve(image)
@ -55,7 +85,7 @@ export const moveImageToFolder = async (pathname, image, dir) => {
fileReader.readAsBinaryString(image)
})
await fse.writeFile(imagePath, binaryString, 'binary')
await fs.writeFile(imagePath, binaryString, 'binary')
return imagePath
}
}
@ -137,11 +167,11 @@ export const uploadImage = async (pathname, image, preferences) => {
const imagePath = path.resolve(dirname, image)
const isImage = isImageFile(imagePath)
if (isImage) {
const { size } = await fse.stat(imagePath)
const { size } = await fs.stat(imagePath)
if (size > MAX_SIZE) {
notification()
} else {
const imageFile = await fse.readFile(imagePath)
const imageFile = await fs.readFile(imagePath)
const blobFile = new Blob([imageFile])
if (currentUploader === 'smms') {
uploadToSMMS(blobFile)

View File

@ -1,5 +1,3 @@
import crypto from 'crypto'
export const delay = time => {
let timerId
let rejectFn
@ -210,14 +208,6 @@ export const cloneObj = (obj, deepCopy = true) => {
return deepCopy ? JSON.parse(JSON.stringify(obj)) : Object.assign({}, obj)
}
export const getHash = (content, encoding, type) => {
return crypto.createHash(type).update(content, encoding).digest('hex')
}
export const getContentHash = content => {
return getHash(content, 'utf8', 'sha1')
}
export const isOsx = process.platform === 'darwin'
export const isWindows = process.platform === 'win32'
export const isLinux = process.platform === 'linux'

View File

@ -31,6 +31,8 @@
"textDirection": "ltr",
"hideQuickInsertHint": false,
"imageInsertAction": "path",
"imagePreferRelativeDirectory": false,
"imageRelativeDirectoryName": "assets",
"hideLinkPopup": false,
"autoCheck": false,