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"
|
"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": {
|
"sideBarVisibility": {
|
||||||
"description": "View--Whether the side bar is visible.",
|
"description": "View--Whether the side bar is visible.",
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
|
@ -74,9 +74,11 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { shell } from 'electron'
|
import { shell } from 'electron'
|
||||||
|
import path from 'path'
|
||||||
import log from 'electron-log'
|
import log from 'electron-log'
|
||||||
import { mapState } from 'vuex'
|
import { mapState } from 'vuex'
|
||||||
// import ViewImage from 'view-image'
|
// import ViewImage from 'view-image'
|
||||||
|
import { isChildOfDirectory } from 'common/filesystem/paths'
|
||||||
import Muya from 'muya/lib'
|
import Muya from 'muya/lib'
|
||||||
import TablePicker from 'muya/lib/ui/tablePicker'
|
import TablePicker from 'muya/lib/ui/tablePicker'
|
||||||
import QuickInsert from 'muya/lib/ui/quickInsert'
|
import QuickInsert from 'muya/lib/ui/quickInsert'
|
||||||
@ -99,7 +101,7 @@ import notice from '@/services/notification'
|
|||||||
import Printer from '@/services/printService'
|
import Printer from '@/services/printService'
|
||||||
import { isOsSpellcheckerSupported, offsetToWordCursor, validateLineCursor, SpellChecker } from '@/spellchecker'
|
import { isOsSpellcheckerSupported, offsetToWordCursor, validateLineCursor, SpellChecker } from '@/spellchecker'
|
||||||
import { delay, isOsx, animatedScrollTo } from '@/util'
|
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 { guessClipboardFilePath } from '@/util/clipboard'
|
||||||
import { getCssForOptions, getHtmlToc } from '@/util/pdf'
|
import { getCssForOptions, getHtmlToc } from '@/util/pdf'
|
||||||
import { addCommonStyle, setEditorWidth } from '@/util/theme'
|
import { addCommonStyle, setEditorWidth } from '@/util/theme'
|
||||||
@ -154,6 +156,8 @@ export default {
|
|||||||
autoCheck: state => state.preferences.autoCheck,
|
autoCheck: state => state.preferences.autoCheck,
|
||||||
editorLineWidth: state => state.preferences.editorLineWidth,
|
editorLineWidth: state => state.preferences.editorLineWidth,
|
||||||
imageInsertAction: state => state.preferences.imageInsertAction,
|
imageInsertAction: state => state.preferences.imageInsertAction,
|
||||||
|
imagePreferRelativeDirectory: state => state.preferences.imagePreferRelativeDirectory,
|
||||||
|
imageRelativeDirectoryName: state => state.preferences.imageRelativeDirectoryName,
|
||||||
imageFolderPath: state => state.preferences.imageFolderPath,
|
imageFolderPath: state => state.preferences.imageFolderPath,
|
||||||
theme: state => state.preferences.theme,
|
theme: state => state.preferences.theme,
|
||||||
sequenceTheme: state => state.preferences.sequenceTheme,
|
sequenceTheme: state => state.preferences.sequenceTheme,
|
||||||
@ -165,6 +169,7 @@ export default {
|
|||||||
spellcheckerLanguage: state => state.preferences.spellcheckerLanguage,
|
spellcheckerLanguage: state => state.preferences.spellcheckerLanguage,
|
||||||
|
|
||||||
currentFile: state => state.editor.currentFile,
|
currentFile: state => state.editor.currentFile,
|
||||||
|
projectTree: state => state.project.projectTree,
|
||||||
|
|
||||||
// edit modes
|
// edit modes
|
||||||
typewriter: state => state.preferences.typewriter,
|
typewriter: state => state.preferences.typewriter,
|
||||||
@ -768,9 +773,26 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async imageAction (image, id, alt = '') {
|
async imageAction (image, id, alt = '') {
|
||||||
const { imageInsertAction, imageFolderPath, preferences } = this
|
const {
|
||||||
|
imageInsertAction,
|
||||||
|
imageFolderPath,
|
||||||
|
imagePreferRelativeDirectory,
|
||||||
|
imageRelativeDirectoryName,
|
||||||
|
preferences
|
||||||
|
} = this
|
||||||
const { pathname } = this.currentFile
|
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) {
|
switch (imageInsertAction) {
|
||||||
case 'upload': {
|
case 'upload': {
|
||||||
try {
|
try {
|
||||||
@ -787,6 +809,9 @@ export default {
|
|||||||
}
|
}
|
||||||
case 'folder': {
|
case 'folder': {
|
||||||
result = await moveImageToFolder(pathname, image, imageFolderPath)
|
result = await moveImageToFolder(pathname, image, imageFolderPath)
|
||||||
|
if (cwd && imagePreferRelativeDirectory) {
|
||||||
|
result = moveToRelativeFolder(cwd, result, imageRelativeDirectoryName)
|
||||||
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'path': {
|
case 'path': {
|
||||||
@ -795,6 +820,11 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
// Move image to image folder if it's Blob object.
|
// Move image to image folder if it's Blob object.
|
||||||
result = await moveImageToFolder(pathname, image, imageFolderPath)
|
result = await moveImageToFolder(pathname, image, imageFolderPath)
|
||||||
|
|
||||||
|
// Respect user preferences if file exist on disk.
|
||||||
|
if (cwd && imagePreferRelativeDirectory) {
|
||||||
|
result = moveToRelativeFolder(cwd, result, imageRelativeDirectoryName)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -2,8 +2,8 @@
|
|||||||
<div class="pref-image">
|
<div class="pref-image">
|
||||||
<h4>Image</h4>
|
<h4>Image</h4>
|
||||||
<section class="image-ctrl">
|
<section class="image-ctrl">
|
||||||
<div>Default action after image is inserted from local folder
|
<div>Default action after image is inserted from local folder or clipboard
|
||||||
<el-tooltip class='item' effect='dark' content='Mark Text can not get image path from paste event on Linux.' placement='top-start'>
|
<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>
|
<i class="el-icon-info"></i>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
</div>
|
</div>
|
||||||
@ -21,23 +21,45 @@
|
|||||||
<el-button size="mini" @click="modifyImageFolderPath">Modify</el-button>
|
<el-button size="mini" @click="modifyImageFolderPath">Modify</el-button>
|
||||||
<el-button size="mini" @click="openImageFolder">Open Folder</el-button>
|
<el-button size="mini" @click="openImageFolder">Open Folder</el-button>
|
||||||
</div>
|
</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>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Separator from '../common/separator'
|
import { mapState } from 'vuex'
|
||||||
import { shell } from 'electron'
|
import { shell } from 'electron'
|
||||||
|
import Bool from '../common/bool'
|
||||||
|
import Separator from '../common/separator'
|
||||||
|
import TextBox from '../common/textBox'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
Separator
|
Bool,
|
||||||
|
Separator,
|
||||||
|
TextBox
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
imagePreferRelativeDirectory: state => state.preferences.imagePreferRelativeDirectory,
|
||||||
|
imageRelativeDirectoryName: state => state.preferences.imageRelativeDirectoryName
|
||||||
|
}),
|
||||||
imageInsertAction: {
|
imageInsertAction: {
|
||||||
get: function () {
|
get: function () {
|
||||||
return this.$store.state.preferences.imageInsertAction
|
return this.$store.state.preferences.imageInsertAction
|
||||||
@ -51,6 +73,11 @@ export default {
|
|||||||
get: function () {
|
get: function () {
|
||||||
return this.$store.state.preferences.imageFolderPath
|
return this.$store.state.preferences.imageFolderPath
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
relativeDirectoryNamePlaceholder: {
|
||||||
|
get: function () {
|
||||||
|
return this.$store.state.preferences.imageRelativeDirectoryName || 'assets'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@ -59,6 +86,9 @@ export default {
|
|||||||
},
|
},
|
||||||
modifyImageFolderPath () {
|
modifyImageFolderPath () {
|
||||||
return this.$store.dispatch('SET_IMAGE_FOLDER_PATH')
|
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',
|
textDirection: 'ltr',
|
||||||
hideQuickInsertHint: false,
|
hideQuickInsertHint: false,
|
||||||
imageInsertAction: 'folder',
|
imageInsertAction: 'folder',
|
||||||
|
imagePreferRelativeDirectory: false,
|
||||||
|
imageRelativeDirectoryName: 'assets',
|
||||||
hideLinkPopup: false,
|
hideLinkPopup: false,
|
||||||
autoCheck: false,
|
autoCheck: false,
|
||||||
|
|
||||||
|
@ -1,29 +1,59 @@
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
import crypto from 'crypto'
|
||||||
import { clipboard } from 'electron'
|
import { clipboard } from 'electron'
|
||||||
import fse from 'fs-extra'
|
import fs from 'fs-extra'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import Octokit from '@octokit/rest'
|
import Octokit from '@octokit/rest'
|
||||||
|
import { ensureDirSync } from 'common/filesystem'
|
||||||
import { isImageFile } from 'common/filesystem/paths'
|
import { isImageFile } from 'common/filesystem/paths'
|
||||||
import { dataURItoBlob, getContentHash } from './index'
|
import { dataURItoBlob } from './index'
|
||||||
import axios from '../axios'
|
import axios from '../axios'
|
||||||
|
|
||||||
export const create = (pathname, type) => {
|
export const create = (pathname, type) => {
|
||||||
if (type === 'directory') {
|
if (type === 'directory') {
|
||||||
return fse.ensureDir(pathname)
|
return fs.ensureDir(pathname)
|
||||||
} else {
|
} else {
|
||||||
return fse.outputFile(pathname, '')
|
return fs.outputFile(pathname, '')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const paste = ({ src, dest, type }) => {
|
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) => {
|
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) => {
|
export const moveImageToFolder = async (pathname, image, dir) => {
|
||||||
|
ensureDirSync(dir)
|
||||||
const isPath = typeof image === 'string'
|
const isPath = typeof image === 'string'
|
||||||
if (isPath) {
|
if (isPath) {
|
||||||
const dirname = path.dirname(pathname)
|
const dirname = path.dirname(pathname)
|
||||||
@ -39,7 +69,7 @@ export const moveImageToFolder = async (pathname, image, dir) => {
|
|||||||
const hash = getContentHash(imagePath)
|
const hash = getContentHash(imagePath)
|
||||||
// To avoid name conflict.
|
// To avoid name conflict.
|
||||||
const hashFilePath = path.join(dir, `${hash}${extname}`)
|
const hashFilePath = path.join(dir, `${hash}${extname}`)
|
||||||
await fse.copy(imagePath, hashFilePath)
|
await fs.copy(imagePath, hashFilePath)
|
||||||
return hashFilePath
|
return hashFilePath
|
||||||
} else {
|
} else {
|
||||||
return Promise.resolve(image)
|
return Promise.resolve(image)
|
||||||
@ -55,7 +85,7 @@ export const moveImageToFolder = async (pathname, image, dir) => {
|
|||||||
|
|
||||||
fileReader.readAsBinaryString(image)
|
fileReader.readAsBinaryString(image)
|
||||||
})
|
})
|
||||||
await fse.writeFile(imagePath, binaryString, 'binary')
|
await fs.writeFile(imagePath, binaryString, 'binary')
|
||||||
return imagePath
|
return imagePath
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -137,11 +167,11 @@ export const uploadImage = async (pathname, image, preferences) => {
|
|||||||
const imagePath = path.resolve(dirname, image)
|
const imagePath = path.resolve(dirname, image)
|
||||||
const isImage = isImageFile(imagePath)
|
const isImage = isImageFile(imagePath)
|
||||||
if (isImage) {
|
if (isImage) {
|
||||||
const { size } = await fse.stat(imagePath)
|
const { size } = await fs.stat(imagePath)
|
||||||
if (size > MAX_SIZE) {
|
if (size > MAX_SIZE) {
|
||||||
notification()
|
notification()
|
||||||
} else {
|
} else {
|
||||||
const imageFile = await fse.readFile(imagePath)
|
const imageFile = await fs.readFile(imagePath)
|
||||||
const blobFile = new Blob([imageFile])
|
const blobFile = new Blob([imageFile])
|
||||||
if (currentUploader === 'smms') {
|
if (currentUploader === 'smms') {
|
||||||
uploadToSMMS(blobFile)
|
uploadToSMMS(blobFile)
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import crypto from 'crypto'
|
|
||||||
|
|
||||||
export const delay = time => {
|
export const delay = time => {
|
||||||
let timerId
|
let timerId
|
||||||
let rejectFn
|
let rejectFn
|
||||||
@ -210,14 +208,6 @@ export const cloneObj = (obj, deepCopy = true) => {
|
|||||||
return deepCopy ? JSON.parse(JSON.stringify(obj)) : Object.assign({}, obj)
|
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 isOsx = process.platform === 'darwin'
|
||||||
export const isWindows = process.platform === 'win32'
|
export const isWindows = process.platform === 'win32'
|
||||||
export const isLinux = process.platform === 'linux'
|
export const isLinux = process.platform === 'linux'
|
||||||
|
@ -31,6 +31,8 @@
|
|||||||
"textDirection": "ltr",
|
"textDirection": "ltr",
|
||||||
"hideQuickInsertHint": false,
|
"hideQuickInsertHint": false,
|
||||||
"imageInsertAction": "path",
|
"imageInsertAction": "path",
|
||||||
|
"imagePreferRelativeDirectory": false,
|
||||||
|
"imageRelativeDirectoryName": "assets",
|
||||||
"hideLinkPopup": false,
|
"hideLinkPopup": false,
|
||||||
"autoCheck": false,
|
"autoCheck": false,
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user