This change adds several features to make it easier to write Markdown articles that desire server-based paths (such as /images/my_image.png).

1) There is now a new preference page under images for "Maintaining server paths". You specify a server path (such as /images/) and a local path (such as c:\hugo\static\images), and
Marktext will do the right mapping internally. Image previews will come from the images on your computer, while the content stored in the document will represent the corresponding server paths.
Full documentation has been added to IMAGES.md.
2) Path variables (like ${filename}) have been expanded to support year, month, and day. This helps prevent filename collisions and is common in many blogging platforms.
3) Pasting images and dragging them into Marktext supports this path mapping as well, moving them to the local path as appropriate
4) Image imports (pasting and or dragging) now retain the source image file name if possible, only using the image hash if there is a conflict. Images with names are easier to manage and show up
appropriately in search engines
4) The image edit dialog now has a 'Rename' tab so that you can rename the images to something more specific when they are just pasted from the clipboard. This simplifies the previous workflow that
used to require editing the markdown and renaming the file in the filesystem manually.

There is also now a 'User Notification Dialog' component to let Marktext communicate with the user about error conditions.
This commit is contained in:
Lee Holmes 2024-01-19 18:42:50 -08:00
parent b75895cdd1
commit 5ffc026a28
26 changed files with 317 additions and 39 deletions

View File

@ -2,19 +2,34 @@
MarkText can automatically copy your images into a specified directory or handle images from clipboard.
### Maintaining server paths during editing and preview
When editing documents, you may want image paths to represent absolute paths on the server that will ultimately host them (such as `/images/myImage.png`) rather than
local filesystem paths (such as `C:\assets\static\images\myImage.png`). You can use _Maintain server paths during editing and preview_ to support this scenario.
When MarkText sees a specified server path for an image in your document, it will instead look into the local path you've provided when previewing images. Your document will retain the original server path as specified.
### 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.
This option automatically copies images into the specified local directory. This path may be a relative path, and may include variables like `${filename}`. The local resource directory is used if the file is not saved. This directory must be a valid path name and MarkText need write access to the directory.
The following are valid variables for use in the image path:
- `${filename}`: The document's file name, without path or extension
- `${year}`: The current year
- `${month}`: The current month, in 2-digit format
- `${day}`: The current day, in 2-digit format
If you have specified the option to _Maintain server paths during editing and preview_, MarkText will replace local filesystem paths with the corresponding server path.
**Prefer relative assets folder:**
When this option is enabled, all images are copied relative to the opened file. The root directory is used when a project is opened and no variables are used. You can specify the path via the *relative image folder name* text box and include variables like `${filename}` to add the file name to the relative directory. The local resource directory is used if the file is not saved.
Note: The assets directory name must be a valid path name and MarkText need write access to the directory.
When this option is enabled, all images are copied relative to the opened file. The root directory is used when a project is opened and no variables are used. You can specify the path via the *relative image folder name* text box.
Examples for relative paths:
@ -23,6 +38,7 @@ Examples for relative paths:
- `.`: current file directory
- `assets/123`
- `assets_${filename}` (add the document file name)
- `assets/${year}/${month}` (save the assets into year and month subdirectories)
### Keep original location

View File

@ -544,6 +544,11 @@ class App {
}
})
ipcMain.on('mt::show-user-notification-dialog', async (e, title, message) => {
const win = BrowserWindow.fromWebContents(e.sender)
win.webContents.send('showUserNotificationDialog', title, message)
})
ipcMain.on('mt::open-setting-window', () => {
this._openSettingsWindow()
})

View File

@ -178,6 +178,26 @@ class DataCenter extends EventEmitter {
}
})
ipcMain.on('mt::ask-for-modify-local-folder-path', async (e, imagePath) => {
if (!imagePath) {
const win = BrowserWindow.fromWebContents(e.sender)
const { filePaths } = await dialog.showOpenDialog(win, {
properties: ['openDirectory', 'createDirectory']
})
if (filePaths && filePaths[0]) {
imagePath = filePaths[0]
}
}
if (imagePath) {
// Ensure they have a path separator at the end of their local folder path
if ((!imagePath.endsWith('/')) && (!imagePath.endsWith('\\'))) {
const pathSeparator = imagePath.replace(/[^\\/]/g, '')[0]
imagePath = imagePath + pathSeparator
}
this.setItem('localFolderPath', imagePath)
}
})
ipcMain.on('mt::set-user-data', (e, userData) => {
this.setItems(userData)
})

View File

@ -349,6 +349,16 @@
],
"default": "path"
},
"serverFolderPath": {
"description": "If specified, a server path of image URLs to maintain during editing and preview",
"type": "string",
"default": ""
},
"localFolderPath": {
"description": "The local path that corresponds to serverFolderPath during editing and preview",
"type": "string",
"default": ""
},
"imagePreferRelativeDirectory": {
"description": "Image--Whether to prefer the relative image directory.",
"type": "boolean",

View File

@ -160,14 +160,14 @@ const dragDropCtrl = ContentState => {
try {
const newSrc = await this.muya.options.imageAction(path, id, name)
const { src } = getImageSrc(path)
const { src } = getImageSrc(path, this.muya.options)
if (src) {
this.stateRender.urlMap.set(newSrc, src)
}
const imageWrapper = this.muya.container.querySelector(`span[data-id=${id}]`)
if (imageWrapper) {
const imageInfo = getImageInfo(imageWrapper)
const imageInfo = getImageInfo(imageWrapper, this.muya.options)
this.replaceImage(imageInfo, {
alt: name,
src: newSrc

View File

@ -284,7 +284,7 @@ const formatCtrl = ContentState => {
if (startNode) {
const imageWrapper = startNode.closest('.ag-inline-image')
if (imageWrapper && imageWrapper.classList.contains('ag-empty-image')) {
const imageInfo = getImageInfo(imageWrapper)
const imageInfo = getImageInfo(imageWrapper, this.muya.options)
this.muya.eventCenter.dispatch('muya-image-selector', {
reference: imageWrapper,
imageInfo,

View File

@ -150,7 +150,7 @@ const pasteCtrl = ContentState => {
return null
}
const { src } = getImageSrc(imagePath)
const { src } = getImageSrc(imagePath, this.muya.options)
if (src) {
this.stateRender.urlMap.set(newSrc, src)
}
@ -158,7 +158,7 @@ const pasteCtrl = ContentState => {
const imageWrapper = this.muya.container.querySelector(`span[data-id=${id}]`)
if (imageWrapper) {
const imageInfo = getImageInfo(imageWrapper)
const imageInfo = getImageInfo(imageWrapper, this.muya.options)
this.replaceImage(imageInfo, {
src: newSrc
})
@ -225,7 +225,7 @@ const pasteCtrl = ContentState => {
const imageWrapper = this.muya.container.querySelector(`span[data-id=${id}]`)
if (imageWrapper) {
const imageInfo = getImageInfo(imageWrapper)
const imageInfo = getImageInfo(imageWrapper, this.muya.options)
this.replaceImage(imageInfo, {
src: newSrc
})

View File

@ -127,7 +127,7 @@ class ClickEvent {
}
// Handle delete inline iamge by click delete icon.
if (imageDelete && imageWrapper) {
const imageInfo = getImageInfo(imageWrapper)
const imageInfo = getImageInfo(imageWrapper, this.muya.options)
event.preventDefault()
event.stopPropagation()
// hide image selector if needed.
@ -152,7 +152,7 @@ class ClickEvent {
// Handle image click, to select the current image
if (target.tagName === 'IMG' && imageWrapper) {
// Handle select image
const imageInfo = getImageInfo(imageWrapper)
const imageInfo = getImageInfo(imageWrapper, this.muya.options)
event.preventDefault()
eventCenter.dispatch('select-image', imageInfo)
// Handle show image toolbar
@ -197,7 +197,7 @@ class ClickEvent {
return rect
}
}
const imageInfo = getImageInfo(imageWrapper)
const imageInfo = getImageInfo(imageWrapper, this.muya.options)
eventCenter.dispatch('muya-image-selector', {
reference,
imageInfo,

View File

@ -103,7 +103,7 @@ class Keyboard {
case EVENT_KEYS.Space: {
if (contentState.selectedImage) {
const { token } = contentState.selectedImage
const { src } = getImageInfo(token.src || token.attrs.src)
const { src } = getImageInfo(token.src || token.attrs.src, this.muya.options)
if (src) {
eventCenter.dispatch('preview-image', {
data: src

View File

@ -23,8 +23,10 @@ class Muya {
})
}
constructor (container, options) {
constructor (container, options, preferences) {
this.options = Object.assign({}, MUYA_DEFAULT_OPTION, options)
this.options = Object.assign(this.options, preferences)
const { markdown } = this.options
this.markdown = markdown
this.container = getContainer(container, this.options)

View File

@ -143,7 +143,7 @@ export default function renderLeafBlock (parent, block, activeBlocks, matches, u
const imgs = doc.documentElement.querySelectorAll('img')
for (const img of imgs) {
const src = img.getAttribute('src')
const imageInfo = getImageInfo(src)
const imageInfo = getImageInfo(src, this.muya.options)
img.setAttribute('src', imageInfo.src)
}

View File

@ -22,7 +22,7 @@ const renderIcon = (h, className, icon) => {
// I dont want operate dom directly, is there any better method? need help!
export default function image (h, cursor, block, token, outerClass) {
const imageInfo = getImageInfo(token.attrs.src)
const imageInfo = getImageInfo(token.attrs.src, this.muya.options)
const { selectedImage } = this.muya.contentState
const data = {
dataset: {

View File

@ -14,7 +14,7 @@ export default function referenceImage (h, cursor, block, token, outerClass) {
if (this.labels.has((rawSrc).toLowerCase())) {
({ href, title } = this.labels.get(rawSrc.toLowerCase()))
}
const imageInfo = getImageInfo(href)
const imageInfo = getImageInfo(href, this.muya.options)
const { src } = imageInfo
let id
let isSuccess

View File

@ -4,6 +4,8 @@ import { patch, h } from '../../parser/render/snabbdom'
import { EVENT_KEYS, URL_REG, isWin } from '../../config'
import { getUniqueId, getImageInfo as getImageSrc } from '../../utils'
import { getImageInfo } from '../../utils/getImageInfo'
import fs from 'fs-extra'
import { ipcRenderer } from 'electron'
import './index.css'
@ -190,6 +192,13 @@ class ImageSelector extends BaseFloat {
}
}
renameInputKeyDown (event) {
if (event.key === EVENT_KEYS.Enter) {
event.stopPropagation()
this.handleRenameButtonClick()
}
}
async handleKeyUp (event) {
const { key } = event
if (
@ -236,6 +245,24 @@ class ImageSelector extends BaseFloat {
return this.replaceImageAsync(this.state)
}
handleRenameButtonClick () {
const oldSrc = this.imageInfo.token.attrs.src
let { src: newLocalPath } = getImageSrc(this.state.src, this.muya.options)
let { src: oldLocalPath } = getImageSrc(oldSrc, this.muya.options)
newLocalPath = newLocalPath.replace('file://', '')
oldLocalPath = oldLocalPath.replace('file://', '')
try {
fs.renameSync(oldLocalPath, newLocalPath)
} catch (error) {
this.state.src = oldSrc
ipcRenderer.send('mt::show-user-notification-dialog', 'Could not rename file', error)
}
return this.replaceImageAsync(this.state)
}
replaceImageAsync = async ({ alt, src, title }) => {
if (!this.muya.options.imageAction || URL_REG.test(src)) {
const { alt: oldAlt, src: oldSrc, title: oldTitle } = this.imageInfo.token.attrs
@ -255,14 +282,14 @@ class ImageSelector extends BaseFloat {
try {
const newSrc = await this.muya.options.imageAction(src, id, alt)
const { src: localPath } = getImageSrc(src)
const { src: localPath } = getImageSrc(src, this.muya.options)
if (localPath) {
this.muya.contentState.stateRender.urlMap.set(newSrc, localPath)
}
const imageWrapper = this.muya.container.querySelector(`span[data-id=${id}]`)
if (imageWrapper) {
const imageInfo = getImageInfo(imageWrapper)
const imageInfo = getImageInfo(imageWrapper, this.muya.options)
this.muya.contentState.replaceImage(imageInfo, {
alt,
src: newSrc,
@ -270,8 +297,7 @@ class ImageSelector extends BaseFloat {
})
}
} catch (error) {
// TODO: Notify user about an error.
console.error('Unexpected error on image action:', error)
ipcRenderer.send('mt::show-user-notification-dialog', 'Error while updating image', error)
}
} else {
this.hide()
@ -302,6 +328,9 @@ class ImageSelector extends BaseFloat {
}, {
label: 'Embed link',
value: 'link'
}, {
label: 'Rename',
value: 'rename'
}]
if (this.unsplash) {
@ -418,6 +447,34 @@ class ImageSelector extends BaseFloat {
}, `${isFullMode ? 'simple mode' : 'full mode'}.`)
])
bodyContent = [inputWrapper, embedButton, bottomDes]
} else if (tab === 'rename') {
const srcInput = h('input.src', {
props: {
placeholder: 'New image link or local path',
value: src
},
on: {
input: event => {
this.inputHandler(event, 'src')
},
paste: event => {
this.inputHandler(event, 'src')
},
keydown: event => {
this.renameInputKeyDown(event)
}
}
})
const inputWrapper = h('div.input-container', [srcInput])
const renameButton = h('button.muya-button.role-button.link', {
on: {
click: event => {
this.handleRenameButtonClick()
}
}
}, 'Rename Image')
bodyContent = [inputWrapper, renameButton]
} else {
const searchInput = h('input.search', {
props: {

View File

@ -611,7 +611,7 @@ const importRegister = ContentState => {
const rawSrc = label + backlash.second
if (render.labels.has((rawSrc).toLowerCase())) {
const { href } = render.labels.get(rawSrc.toLowerCase())
const { src } = getImageInfo(href)
const { src } = getImageInfo(href, this.muya.options)
if (src) {
results.add(src)
}

View File

@ -251,14 +251,30 @@ export const checkImageContentType = url => {
* Return image information and correct the relative image path if needed.
*
* @param {string} src Image url
* @param {string} baseUrl Base path; used on desktop to fix the relative image path.
* @param {object} options The muya options object representing user preferences.
*/
export const getImageInfo = (src, baseUrl = window.DIRNAME) => {
export const getImageInfo = (src, options) => {
const baseUrl = window.DIRNAME
const imageExtension = IMAGE_EXT_REG.test(src)
const isUrl = URL_REG.test(src) || (imageExtension && /^file:\/\/.+/.test(src))
// Treat an URL with valid extension as image.
if (imageExtension) {
if (options) {
const { serverFolderPath, localFolderPath } = options
// Do server / local path mapping during preview if desired
if (serverFolderPath) {
if (src.toLowerCase().includes(serverFolderPath.toLowerCase())) {
const escapedServerFolderPath = serverFolderPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
const regex = new RegExp(escapedServerFolderPath, 'ig')
src = src.replace(regex, localFolderPath)
src = src.replace('//', '/')
src = src.replace('\\\\', '\\')
}
}
}
// NOTE: Check both "C:\" and "C:/" because we're using "file:///C:/".
const isAbsoluteLocal = /^(?:\/|\\\\|[a-zA-Z]:\\|[a-zA-Z]:\/).+/.test(src)
@ -272,11 +288,16 @@ export const getImageInfo = (src, baseUrl = window.DIRNAME) => {
src
}
} else {
// Correct relative path on desktop. If we resolve a absolute path "path.resolve" doesn't do anything.
// NOTE: We don't need to convert Windows styled path to UNIX style because Chromium handels this internal.
// Correct relative path on desktop.
// NOTE: We don't need to convert Windows styled path to UNIX style because Chromium handles this internally.
if (!isAbsoluteLocal) {
src = require('path').resolve(baseUrl, src)
} else {
src = require('path').resolve(src)
}
return {
isUnknownType: false,
src: 'file://' + require('path').resolve(baseUrl, src)
src: 'file://' + src
}
}
} else if (isUrl && !imageExtension) {

View File

@ -156,6 +156,8 @@ export default {
autoCheck: state => state.preferences.autoCheck,
editorLineWidth: state => state.preferences.editorLineWidth,
imageInsertAction: state => state.preferences.imageInsertAction,
serverFolderPath: state => state.preferences.serverFolderPath,
localFolderPath: state => state.preferences.localFolderPath,
imagePreferRelativeDirectory: state => state.preferences.imagePreferRelativeDirectory,
imageRelativeDirectoryName: state => state.preferences.imageRelativeDirectoryName,
imageFolderPath: state => state.preferences.imageFolderPath,
@ -549,7 +551,7 @@ export default {
})
}
const { container } = this.editor = new Muya(ele, options)
const { container } = this.editor = new Muya(ele, options, this.$store.state.preferences)
// Create spell check wrapper and enable spell checking if preferred.
this.spellchecker = new SpellChecker(spellcheckerEnabled, spellcheckerLanguage)
@ -717,7 +719,15 @@ export default {
// Filename w/o extension
? filename.replace(/\.[^/.]+$/, '')
: ''
return imagePath.replace(/\${filename}/g, replacement)
imagePath = imagePath.replace(/\${filename}/g, replacement)
// Support year (numeric), month (2-digit), and day (2-digit) for image path replacements
const today = new Date()
imagePath = imagePath.replace(/\${year}/g, today.toLocaleDateString('en-US', { year: 'numeric' }))
imagePath = imagePath.replace(/\${month}/g, today.toLocaleDateString('en-US', { month: '2-digit' }))
imagePath = imagePath.replace(/\${day}/g, today.toLocaleDateString('en-US', { day: '2-digit' }))
return imagePath
}
const resolvedImageFolderPath = getResolvedImagePath(imageFolderPath)
@ -768,6 +778,20 @@ export default {
alt
})
}
// Map local paths to server paths if the user desires
if (this.serverFolderPath) {
if (destImagePath.toLowerCase().includes(this.localFolderPath.toLowerCase())) {
const escapedLocalFolderPath = this.localFolderPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
const regex = new RegExp(escapedLocalFolderPath, 'ig')
destImagePath = destImagePath.replace(regex, this.serverFolderPath)
// Convert slash direction since the user is wanting URLs to represent server paths
destImagePath = destImagePath.replace(/\\/g, '/')
destImagePath = destImagePath.replace(/\/\//g, '/')
}
}
return destImagePath
},

View File

@ -0,0 +1,50 @@
<template>
<div class="user-notification-dialog">
<el-dialog
:visible.sync="showUserNotificationDialog"
:show-close="true"
:modal="true"
custom-class="ag-dialog-table"
width="400px"
>
<h3>{{ title }}</h3>
<span class="text">{{ message }}</span>
</el-dialog>
</div>
</template>
<script>
import bus from '../../bus'
export default {
data () {
this.title = ''
this.message = ''
return {
showUserNotificationDialog: false
}
},
created () {
require('electron').ipcRenderer.on('showUserNotificationDialog', this.showDialog)
},
beforeDestroy () {
require('electron').ipcRenderer.off('showUserNotificationDialog', this.showDialog)
},
methods: {
showDialog (e, title, message) {
this.title = title
this.message = message
this.showUserNotificationDialog = true
bus.$emit('editor-blur')
}
}
}
</script>
<style scoped>
.text {
white-space: pre-wrap;
word-break: break-word;
}
</style>

View File

@ -31,6 +31,7 @@
<export-setting-dialog></export-setting-dialog>
<rename></rename>
<tweet></tweet>
<userNotification></userNotification>
<import-modal></import-modal>
</div>
</div>
@ -48,6 +49,7 @@ import ExportSettingDialog from '@/components/exportSettings'
import Rename from '@/components/rename'
import Tweet from '@/components/tweet'
import ImportModal from '@/components/import'
import UserNotification from '@/components/userNotification'
import { loadingPageMixins } from '@/mixins'
import { mapState } from 'vuex'
import bus from '@/bus'
@ -65,6 +67,7 @@ export default {
ExportSettingDialog,
Rename,
Tweet,
UserNotification,
ImportModal,
CommandPalette
},

View File

@ -1,6 +1,9 @@
<template>
<section class="image-folder">
<h5>Global or relative image folder</h5>
<div class="footnote">
Use variables such as <code>${filename}</code> in paths to automatically insert the document file name.
</div>
<text-box description="Global image folder" :input="imageFolderPath"
:regexValidator="/^(?:$|([a-zA-Z]:)?[\/\\].*$)/" :defaultValue="folderPathPlaceholder"
:onChange="value => modifyImageFolderPath(value)"></text-box>
@ -20,9 +23,6 @@
:regexValidator="/^(?:$|(?![a-zA-Z]:)[^\/\\].*$)/"
:defaultValue="relativeDirectoryNamePlaceholder"
:onChange="value => onSelectChange('imageRelativeDirectoryName', value)"></text-box>
<div class="footnote">
Include <code>${filename}</code> in the text-box above to automatically insert the document file name.
</div>
</template>
</compound>
</section>

View File

@ -0,0 +1,50 @@
<template>
<section class="server-path-folder">
<h5>Maintain server paths during editing and preview</h5>
<text-box description="Server path (i.e.: /images/)" :input="serverFolderPath"
:onChange="value => onSelectChange('serverFolderPath', value)"></text-box>
<text-box description="Local path (relative or absolute)" :input="localFolderPath"
:onChange="value => modifyLocalFolderPath(value)"></text-box>
<div>
<el-button size="mini" @click="modifyLocalFolderPath(undefined)">Browse...</el-button>
</div>
</section>
</template>
<script>
import { mapState } from 'vuex'
import TextBox from '@/prefComponents/common/textBox'
export default {
components: {
TextBox
},
data () {
return {
}
},
computed: {
...mapState({
serverFolderPath: state => state.preferences.serverFolderPath,
localFolderPath: state => state.preferences.localFolderPath
})
},
methods: {
modifyLocalFolderPath (value) {
return this.$store.dispatch('SET_LOCAL_FOLDER_PATH', value)
},
onSelectChange (type, value) {
this.$store.dispatch('SET_SINGLE_PREFERENCE', { type, value })
}
}
}
</script>
<style scoped>
.image-folder .footnote {
font-size: 13px;
& code {
font-size: 13px;
}
}
</style>

View File

@ -13,6 +13,8 @@
:onChange="value => onSelectChange('imageInsertAction', value)"></CurSelect>
</section>
<Separator />
<ServerPathSetting v-if="imageInsertAction === 'folder' || imageInsertAction === 'path'" />
<Separator />
<FolderSetting v-if="imageInsertAction === 'folder' || imageInsertAction === 'path'" />
<Uploader v-if="imageInsertAction === 'upload'" />
</div>
@ -23,12 +25,14 @@ import Separator from '../common/separator'
import Uploader from './components/uploader'
import CurSelect from '@/prefComponents/common/select'
import FolderSetting from './components/folderSetting'
import ServerPathSetting from './components/serverPathSetting'
import { imageActions } from './config'
export default {
components: {
Separator,
CurSelect,
ServerPathSetting,
FolderSetting,
Uploader
},

View File

@ -20,7 +20,7 @@ class MarkdownPrint {
const images = printContainer.getElementsByTagName('img')
for (const image of images) {
const rawSrc = image.getAttribute('src')
image.src = getImageInfo(rawSrc).src
image.src = getImageInfo(rawSrc, this.muya.options).src
}
}
document.body.appendChild(printContainer)

View File

@ -35,6 +35,8 @@ const state = {
textDirection: 'ltr',
hideQuickInsertHint: false,
imageInsertAction: 'folder',
serverFolderPath: '',
localFolderPath: '',
imagePreferRelativeDirectory: false,
imageRelativeDirectoryName: 'assets',
hideLinkPopup: false,
@ -137,6 +139,10 @@ const actions = {
ipcRenderer.send('mt::ask-for-modify-image-folder-path', value)
},
SET_LOCAL_FOLDER_PATH ({ commit }, value) {
ipcRenderer.send('mt::ask-for-modify-local-folder-path', value)
},
SELECT_DEFAULT_DIRECTORY_TO_OPEN ({ commit }) {
ipcRenderer.send('mt::select-default-directory-to-open')
},

View File

@ -79,14 +79,22 @@ export const moveImageToFolder = async (pathname, image, outputDir) => {
const filename = path.basename(imagePath)
const extname = path.extname(imagePath)
const noHashPath = path.join(outputDir, filename)
// If the file doesn't need to be moved, return it
if (noHashPath === imagePath) {
return imagePath
}
let targetPath = noHashPath
if (fs.existsSync(targetPath)) {
// If a file already exists in the target location, use the image's content hash to avoid
// name conflict.
const hash = getContentHash(imagePath)
// To avoid name conflict.
const hashFilePath = path.join(outputDir, `${hash}${extname}`)
await fs.copy(imagePath, hashFilePath)
return hashFilePath
targetPath = path.join(outputDir, `${hash}${extname}`)
}
await fs.copy(imagePath, targetPath)
return targetPath
} else {
return Promise.resolve(image)
}

View File

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