mirror of
https://github.com/marktext/marktext.git
synced 2025-05-02 07:31:18 +08:00
Add support for relative image directory (#2439)
This commit is contained in:
parent
38935cc40c
commit
2b53a414f9
28
docs/IMAGES.md
Normal file
28
docs/IMAGES.md
Normal 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.
|
@ -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",
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -35,6 +35,8 @@ const state = {
|
||||
textDirection: 'ltr',
|
||||
hideQuickInsertHint: false,
|
||||
imageInsertAction: 'folder',
|
||||
imagePreferRelativeDirectory: false,
|
||||
imageRelativeDirectoryName: 'assets',
|
||||
hideLinkPopup: false,
|
||||
autoCheck: false,
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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'
|
||||
|
@ -31,6 +31,8 @@
|
||||
"textDirection": "ltr",
|
||||
"hideQuickInsertHint": false,
|
||||
"imageInsertAction": "path",
|
||||
"imagePreferRelativeDirectory": false,
|
||||
"imageRelativeDirectoryName": "assets",
|
||||
"hideLinkPopup": false,
|
||||
"autoCheck": false,
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user