mirror of
https://github.com/marktext/marktext.git
synced 2025-05-02 20:12:23 +08:00
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:
parent
b75895cdd1
commit
5ffc026a28
@ -2,19 +2,34 @@
|
|||||||
|
|
||||||
MarkText can automatically copy your images into a specified directory or handle images from clipboard.
|
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
|
### Upload to cloud using selected uploader
|
||||||
|
|
||||||
Please see [here](IMAGE_UPLOADER_CONFIGRATION.md) for more information.
|
Please see [here](IMAGE_UPLOADER_CONFIGRATION.md) for more information.
|
||||||
|
|
||||||
### Move to designated local folder
|
### 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:**
|
**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.
|
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.
|
||||||
|
|
||||||
Note: The assets directory name must be a valid path name and MarkText need write access to the directory.
|
|
||||||
|
|
||||||
Examples for relative paths:
|
Examples for relative paths:
|
||||||
|
|
||||||
@ -23,6 +38,7 @@ Examples for relative paths:
|
|||||||
- `.`: current file directory
|
- `.`: current file directory
|
||||||
- `assets/123`
|
- `assets/123`
|
||||||
- `assets_${filename}` (add the document file name)
|
- `assets_${filename}` (add the document file name)
|
||||||
|
- `assets/${year}/${month}` (save the assets into year and month subdirectories)
|
||||||
|
|
||||||
### Keep original location
|
### Keep original location
|
||||||
|
|
||||||
|
@ -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', () => {
|
ipcMain.on('mt::open-setting-window', () => {
|
||||||
this._openSettingsWindow()
|
this._openSettingsWindow()
|
||||||
})
|
})
|
||||||
|
@ -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) => {
|
ipcMain.on('mt::set-user-data', (e, userData) => {
|
||||||
this.setItems(userData)
|
this.setItems(userData)
|
||||||
})
|
})
|
||||||
|
@ -349,6 +349,16 @@
|
|||||||
],
|
],
|
||||||
"default": "path"
|
"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": {
|
"imagePreferRelativeDirectory": {
|
||||||
"description": "Image--Whether to prefer the relative image directory.",
|
"description": "Image--Whether to prefer the relative image directory.",
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
|
@ -160,14 +160,14 @@ const dragDropCtrl = ContentState => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const newSrc = await this.muya.options.imageAction(path, id, name)
|
const newSrc = await this.muya.options.imageAction(path, id, name)
|
||||||
const { src } = getImageSrc(path)
|
const { src } = getImageSrc(path, this.muya.options)
|
||||||
if (src) {
|
if (src) {
|
||||||
this.stateRender.urlMap.set(newSrc, src)
|
this.stateRender.urlMap.set(newSrc, src)
|
||||||
}
|
}
|
||||||
const imageWrapper = this.muya.container.querySelector(`span[data-id=${id}]`)
|
const imageWrapper = this.muya.container.querySelector(`span[data-id=${id}]`)
|
||||||
|
|
||||||
if (imageWrapper) {
|
if (imageWrapper) {
|
||||||
const imageInfo = getImageInfo(imageWrapper)
|
const imageInfo = getImageInfo(imageWrapper, this.muya.options)
|
||||||
this.replaceImage(imageInfo, {
|
this.replaceImage(imageInfo, {
|
||||||
alt: name,
|
alt: name,
|
||||||
src: newSrc
|
src: newSrc
|
||||||
|
@ -284,7 +284,7 @@ const formatCtrl = ContentState => {
|
|||||||
if (startNode) {
|
if (startNode) {
|
||||||
const imageWrapper = startNode.closest('.ag-inline-image')
|
const imageWrapper = startNode.closest('.ag-inline-image')
|
||||||
if (imageWrapper && imageWrapper.classList.contains('ag-empty-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', {
|
this.muya.eventCenter.dispatch('muya-image-selector', {
|
||||||
reference: imageWrapper,
|
reference: imageWrapper,
|
||||||
imageInfo,
|
imageInfo,
|
||||||
|
@ -150,7 +150,7 @@ const pasteCtrl = ContentState => {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const { src } = getImageSrc(imagePath)
|
const { src } = getImageSrc(imagePath, this.muya.options)
|
||||||
if (src) {
|
if (src) {
|
||||||
this.stateRender.urlMap.set(newSrc, src)
|
this.stateRender.urlMap.set(newSrc, src)
|
||||||
}
|
}
|
||||||
@ -158,7 +158,7 @@ const pasteCtrl = ContentState => {
|
|||||||
const imageWrapper = this.muya.container.querySelector(`span[data-id=${id}]`)
|
const imageWrapper = this.muya.container.querySelector(`span[data-id=${id}]`)
|
||||||
|
|
||||||
if (imageWrapper) {
|
if (imageWrapper) {
|
||||||
const imageInfo = getImageInfo(imageWrapper)
|
const imageInfo = getImageInfo(imageWrapper, this.muya.options)
|
||||||
this.replaceImage(imageInfo, {
|
this.replaceImage(imageInfo, {
|
||||||
src: newSrc
|
src: newSrc
|
||||||
})
|
})
|
||||||
@ -225,7 +225,7 @@ const pasteCtrl = ContentState => {
|
|||||||
const imageWrapper = this.muya.container.querySelector(`span[data-id=${id}]`)
|
const imageWrapper = this.muya.container.querySelector(`span[data-id=${id}]`)
|
||||||
|
|
||||||
if (imageWrapper) {
|
if (imageWrapper) {
|
||||||
const imageInfo = getImageInfo(imageWrapper)
|
const imageInfo = getImageInfo(imageWrapper, this.muya.options)
|
||||||
this.replaceImage(imageInfo, {
|
this.replaceImage(imageInfo, {
|
||||||
src: newSrc
|
src: newSrc
|
||||||
})
|
})
|
||||||
|
@ -127,7 +127,7 @@ class ClickEvent {
|
|||||||
}
|
}
|
||||||
// Handle delete inline iamge by click delete icon.
|
// Handle delete inline iamge by click delete icon.
|
||||||
if (imageDelete && imageWrapper) {
|
if (imageDelete && imageWrapper) {
|
||||||
const imageInfo = getImageInfo(imageWrapper)
|
const imageInfo = getImageInfo(imageWrapper, this.muya.options)
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
// hide image selector if needed.
|
// hide image selector if needed.
|
||||||
@ -152,7 +152,7 @@ class ClickEvent {
|
|||||||
// Handle image click, to select the current image
|
// Handle image click, to select the current image
|
||||||
if (target.tagName === 'IMG' && imageWrapper) {
|
if (target.tagName === 'IMG' && imageWrapper) {
|
||||||
// Handle select image
|
// Handle select image
|
||||||
const imageInfo = getImageInfo(imageWrapper)
|
const imageInfo = getImageInfo(imageWrapper, this.muya.options)
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
eventCenter.dispatch('select-image', imageInfo)
|
eventCenter.dispatch('select-image', imageInfo)
|
||||||
// Handle show image toolbar
|
// Handle show image toolbar
|
||||||
@ -197,7 +197,7 @@ class ClickEvent {
|
|||||||
return rect
|
return rect
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const imageInfo = getImageInfo(imageWrapper)
|
const imageInfo = getImageInfo(imageWrapper, this.muya.options)
|
||||||
eventCenter.dispatch('muya-image-selector', {
|
eventCenter.dispatch('muya-image-selector', {
|
||||||
reference,
|
reference,
|
||||||
imageInfo,
|
imageInfo,
|
||||||
|
@ -103,7 +103,7 @@ class Keyboard {
|
|||||||
case EVENT_KEYS.Space: {
|
case EVENT_KEYS.Space: {
|
||||||
if (contentState.selectedImage) {
|
if (contentState.selectedImage) {
|
||||||
const { token } = 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) {
|
if (src) {
|
||||||
eventCenter.dispatch('preview-image', {
|
eventCenter.dispatch('preview-image', {
|
||||||
data: src
|
data: src
|
||||||
|
@ -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({}, MUYA_DEFAULT_OPTION, options)
|
||||||
|
this.options = Object.assign(this.options, preferences)
|
||||||
|
|
||||||
const { markdown } = this.options
|
const { markdown } = this.options
|
||||||
this.markdown = markdown
|
this.markdown = markdown
|
||||||
this.container = getContainer(container, this.options)
|
this.container = getContainer(container, this.options)
|
||||||
|
@ -143,7 +143,7 @@ export default function renderLeafBlock (parent, block, activeBlocks, matches, u
|
|||||||
const imgs = doc.documentElement.querySelectorAll('img')
|
const imgs = doc.documentElement.querySelectorAll('img')
|
||||||
for (const img of imgs) {
|
for (const img of imgs) {
|
||||||
const src = img.getAttribute('src')
|
const src = img.getAttribute('src')
|
||||||
const imageInfo = getImageInfo(src)
|
const imageInfo = getImageInfo(src, this.muya.options)
|
||||||
img.setAttribute('src', imageInfo.src)
|
img.setAttribute('src', imageInfo.src)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@ const renderIcon = (h, className, icon) => {
|
|||||||
|
|
||||||
// I dont want operate dom directly, is there any better method? need help!
|
// I dont want operate dom directly, is there any better method? need help!
|
||||||
export default function image (h, cursor, block, token, outerClass) {
|
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 { selectedImage } = this.muya.contentState
|
||||||
const data = {
|
const data = {
|
||||||
dataset: {
|
dataset: {
|
||||||
|
@ -14,7 +14,7 @@ export default function referenceImage (h, cursor, block, token, outerClass) {
|
|||||||
if (this.labels.has((rawSrc).toLowerCase())) {
|
if (this.labels.has((rawSrc).toLowerCase())) {
|
||||||
({ href, title } = this.labels.get(rawSrc.toLowerCase()))
|
({ href, title } = this.labels.get(rawSrc.toLowerCase()))
|
||||||
}
|
}
|
||||||
const imageInfo = getImageInfo(href)
|
const imageInfo = getImageInfo(href, this.muya.options)
|
||||||
const { src } = imageInfo
|
const { src } = imageInfo
|
||||||
let id
|
let id
|
||||||
let isSuccess
|
let isSuccess
|
||||||
|
@ -4,6 +4,8 @@ import { patch, h } from '../../parser/render/snabbdom'
|
|||||||
import { EVENT_KEYS, URL_REG, isWin } from '../../config'
|
import { EVENT_KEYS, URL_REG, isWin } from '../../config'
|
||||||
import { getUniqueId, getImageInfo as getImageSrc } from '../../utils'
|
import { getUniqueId, getImageInfo as getImageSrc } from '../../utils'
|
||||||
import { getImageInfo } from '../../utils/getImageInfo'
|
import { getImageInfo } from '../../utils/getImageInfo'
|
||||||
|
import fs from 'fs-extra'
|
||||||
|
import { ipcRenderer } from 'electron'
|
||||||
|
|
||||||
import './index.css'
|
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) {
|
async handleKeyUp (event) {
|
||||||
const { key } = event
|
const { key } = event
|
||||||
if (
|
if (
|
||||||
@ -236,6 +245,24 @@ class ImageSelector extends BaseFloat {
|
|||||||
return this.replaceImageAsync(this.state)
|
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 }) => {
|
replaceImageAsync = async ({ alt, src, title }) => {
|
||||||
if (!this.muya.options.imageAction || URL_REG.test(src)) {
|
if (!this.muya.options.imageAction || URL_REG.test(src)) {
|
||||||
const { alt: oldAlt, src: oldSrc, title: oldTitle } = this.imageInfo.token.attrs
|
const { alt: oldAlt, src: oldSrc, title: oldTitle } = this.imageInfo.token.attrs
|
||||||
@ -255,14 +282,14 @@ class ImageSelector extends BaseFloat {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const newSrc = await this.muya.options.imageAction(src, id, alt)
|
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) {
|
if (localPath) {
|
||||||
this.muya.contentState.stateRender.urlMap.set(newSrc, localPath)
|
this.muya.contentState.stateRender.urlMap.set(newSrc, localPath)
|
||||||
}
|
}
|
||||||
const imageWrapper = this.muya.container.querySelector(`span[data-id=${id}]`)
|
const imageWrapper = this.muya.container.querySelector(`span[data-id=${id}]`)
|
||||||
|
|
||||||
if (imageWrapper) {
|
if (imageWrapper) {
|
||||||
const imageInfo = getImageInfo(imageWrapper)
|
const imageInfo = getImageInfo(imageWrapper, this.muya.options)
|
||||||
this.muya.contentState.replaceImage(imageInfo, {
|
this.muya.contentState.replaceImage(imageInfo, {
|
||||||
alt,
|
alt,
|
||||||
src: newSrc,
|
src: newSrc,
|
||||||
@ -270,8 +297,7 @@ class ImageSelector extends BaseFloat {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// TODO: Notify user about an error.
|
ipcRenderer.send('mt::show-user-notification-dialog', 'Error while updating image', error)
|
||||||
console.error('Unexpected error on image action:', error)
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.hide()
|
this.hide()
|
||||||
@ -302,6 +328,9 @@ class ImageSelector extends BaseFloat {
|
|||||||
}, {
|
}, {
|
||||||
label: 'Embed link',
|
label: 'Embed link',
|
||||||
value: 'link'
|
value: 'link'
|
||||||
|
}, {
|
||||||
|
label: 'Rename',
|
||||||
|
value: 'rename'
|
||||||
}]
|
}]
|
||||||
|
|
||||||
if (this.unsplash) {
|
if (this.unsplash) {
|
||||||
@ -418,6 +447,34 @@ class ImageSelector extends BaseFloat {
|
|||||||
}, `${isFullMode ? 'simple mode' : 'full mode'}.`)
|
}, `${isFullMode ? 'simple mode' : 'full mode'}.`)
|
||||||
])
|
])
|
||||||
bodyContent = [inputWrapper, embedButton, bottomDes]
|
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 {
|
} else {
|
||||||
const searchInput = h('input.search', {
|
const searchInput = h('input.search', {
|
||||||
props: {
|
props: {
|
||||||
|
@ -611,7 +611,7 @@ const importRegister = ContentState => {
|
|||||||
const rawSrc = label + backlash.second
|
const rawSrc = label + backlash.second
|
||||||
if (render.labels.has((rawSrc).toLowerCase())) {
|
if (render.labels.has((rawSrc).toLowerCase())) {
|
||||||
const { href } = render.labels.get(rawSrc.toLowerCase())
|
const { href } = render.labels.get(rawSrc.toLowerCase())
|
||||||
const { src } = getImageInfo(href)
|
const { src } = getImageInfo(href, this.muya.options)
|
||||||
if (src) {
|
if (src) {
|
||||||
results.add(src)
|
results.add(src)
|
||||||
}
|
}
|
||||||
|
@ -251,14 +251,30 @@ export const checkImageContentType = url => {
|
|||||||
* Return image information and correct the relative image path if needed.
|
* Return image information and correct the relative image path if needed.
|
||||||
*
|
*
|
||||||
* @param {string} src Image url
|
* @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 imageExtension = IMAGE_EXT_REG.test(src)
|
||||||
const isUrl = URL_REG.test(src) || (imageExtension && /^file:\/\/.+/.test(src))
|
const isUrl = URL_REG.test(src) || (imageExtension && /^file:\/\/.+/.test(src))
|
||||||
|
|
||||||
// Treat an URL with valid extension as image.
|
// Treat an URL with valid extension as image.
|
||||||
if (imageExtension) {
|
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:/".
|
// NOTE: Check both "C:\" and "C:/" because we're using "file:///C:/".
|
||||||
const isAbsoluteLocal = /^(?:\/|\\\\|[a-zA-Z]:\\|[a-zA-Z]:\/).+/.test(src)
|
const isAbsoluteLocal = /^(?:\/|\\\\|[a-zA-Z]:\\|[a-zA-Z]:\/).+/.test(src)
|
||||||
|
|
||||||
@ -272,11 +288,16 @@ export const getImageInfo = (src, baseUrl = window.DIRNAME) => {
|
|||||||
src
|
src
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Correct relative path on desktop. If we resolve a absolute path "path.resolve" doesn't do anything.
|
// Correct relative path on desktop.
|
||||||
// NOTE: We don't need to convert Windows styled path to UNIX style because Chromium handels this internal.
|
// 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 {
|
return {
|
||||||
isUnknownType: false,
|
isUnknownType: false,
|
||||||
src: 'file://' + require('path').resolve(baseUrl, src)
|
src: 'file://' + src
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (isUrl && !imageExtension) {
|
} else if (isUrl && !imageExtension) {
|
||||||
|
@ -156,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,
|
||||||
|
serverFolderPath: state => state.preferences.serverFolderPath,
|
||||||
|
localFolderPath: state => state.preferences.localFolderPath,
|
||||||
imagePreferRelativeDirectory: state => state.preferences.imagePreferRelativeDirectory,
|
imagePreferRelativeDirectory: state => state.preferences.imagePreferRelativeDirectory,
|
||||||
imageRelativeDirectoryName: state => state.preferences.imageRelativeDirectoryName,
|
imageRelativeDirectoryName: state => state.preferences.imageRelativeDirectoryName,
|
||||||
imageFolderPath: state => state.preferences.imageFolderPath,
|
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.
|
// Create spell check wrapper and enable spell checking if preferred.
|
||||||
this.spellchecker = new SpellChecker(spellcheckerEnabled, spellcheckerLanguage)
|
this.spellchecker = new SpellChecker(spellcheckerEnabled, spellcheckerLanguage)
|
||||||
@ -717,7 +719,15 @@ export default {
|
|||||||
// Filename w/o extension
|
// Filename w/o extension
|
||||||
? filename.replace(/\.[^/.]+$/, '')
|
? 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)
|
const resolvedImageFolderPath = getResolvedImagePath(imageFolderPath)
|
||||||
@ -768,6 +778,20 @@ export default {
|
|||||||
alt
|
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
|
return destImagePath
|
||||||
},
|
},
|
||||||
|
|
||||||
|
50
src/renderer/components/userNotification/index.vue
Normal file
50
src/renderer/components/userNotification/index.vue
Normal 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>
|
@ -31,6 +31,7 @@
|
|||||||
<export-setting-dialog></export-setting-dialog>
|
<export-setting-dialog></export-setting-dialog>
|
||||||
<rename></rename>
|
<rename></rename>
|
||||||
<tweet></tweet>
|
<tweet></tweet>
|
||||||
|
<userNotification></userNotification>
|
||||||
<import-modal></import-modal>
|
<import-modal></import-modal>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -48,6 +49,7 @@ import ExportSettingDialog from '@/components/exportSettings'
|
|||||||
import Rename from '@/components/rename'
|
import Rename from '@/components/rename'
|
||||||
import Tweet from '@/components/tweet'
|
import Tweet from '@/components/tweet'
|
||||||
import ImportModal from '@/components/import'
|
import ImportModal from '@/components/import'
|
||||||
|
import UserNotification from '@/components/userNotification'
|
||||||
import { loadingPageMixins } from '@/mixins'
|
import { loadingPageMixins } from '@/mixins'
|
||||||
import { mapState } from 'vuex'
|
import { mapState } from 'vuex'
|
||||||
import bus from '@/bus'
|
import bus from '@/bus'
|
||||||
@ -65,6 +67,7 @@ export default {
|
|||||||
ExportSettingDialog,
|
ExportSettingDialog,
|
||||||
Rename,
|
Rename,
|
||||||
Tweet,
|
Tweet,
|
||||||
|
UserNotification,
|
||||||
ImportModal,
|
ImportModal,
|
||||||
CommandPalette
|
CommandPalette
|
||||||
},
|
},
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="image-folder">
|
<section class="image-folder">
|
||||||
<h5>Global or relative image folder</h5>
|
<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"
|
<text-box description="Global image folder" :input="imageFolderPath"
|
||||||
:regexValidator="/^(?:$|([a-zA-Z]:)?[\/\\].*$)/" :defaultValue="folderPathPlaceholder"
|
:regexValidator="/^(?:$|([a-zA-Z]:)?[\/\\].*$)/" :defaultValue="folderPathPlaceholder"
|
||||||
:onChange="value => modifyImageFolderPath(value)"></text-box>
|
:onChange="value => modifyImageFolderPath(value)"></text-box>
|
||||||
@ -20,9 +23,6 @@
|
|||||||
:regexValidator="/^(?:$|(?![a-zA-Z]:)[^\/\\].*$)/"
|
:regexValidator="/^(?:$|(?![a-zA-Z]:)[^\/\\].*$)/"
|
||||||
:defaultValue="relativeDirectoryNamePlaceholder"
|
:defaultValue="relativeDirectoryNamePlaceholder"
|
||||||
:onChange="value => onSelectChange('imageRelativeDirectoryName', value)"></text-box>
|
: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>
|
</template>
|
||||||
</compound>
|
</compound>
|
||||||
</section>
|
</section>
|
||||||
|
@ -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>
|
@ -13,6 +13,8 @@
|
|||||||
:onChange="value => onSelectChange('imageInsertAction', value)"></CurSelect>
|
:onChange="value => onSelectChange('imageInsertAction', value)"></CurSelect>
|
||||||
</section>
|
</section>
|
||||||
<Separator />
|
<Separator />
|
||||||
|
<ServerPathSetting v-if="imageInsertAction === 'folder' || imageInsertAction === 'path'" />
|
||||||
|
<Separator />
|
||||||
<FolderSetting v-if="imageInsertAction === 'folder' || imageInsertAction === 'path'" />
|
<FolderSetting v-if="imageInsertAction === 'folder' || imageInsertAction === 'path'" />
|
||||||
<Uploader v-if="imageInsertAction === 'upload'" />
|
<Uploader v-if="imageInsertAction === 'upload'" />
|
||||||
</div>
|
</div>
|
||||||
@ -23,12 +25,14 @@ import Separator from '../common/separator'
|
|||||||
import Uploader from './components/uploader'
|
import Uploader from './components/uploader'
|
||||||
import CurSelect from '@/prefComponents/common/select'
|
import CurSelect from '@/prefComponents/common/select'
|
||||||
import FolderSetting from './components/folderSetting'
|
import FolderSetting from './components/folderSetting'
|
||||||
|
import ServerPathSetting from './components/serverPathSetting'
|
||||||
import { imageActions } from './config'
|
import { imageActions } from './config'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
Separator,
|
Separator,
|
||||||
CurSelect,
|
CurSelect,
|
||||||
|
ServerPathSetting,
|
||||||
FolderSetting,
|
FolderSetting,
|
||||||
Uploader
|
Uploader
|
||||||
},
|
},
|
||||||
|
@ -20,7 +20,7 @@ class MarkdownPrint {
|
|||||||
const images = printContainer.getElementsByTagName('img')
|
const images = printContainer.getElementsByTagName('img')
|
||||||
for (const image of images) {
|
for (const image of images) {
|
||||||
const rawSrc = image.getAttribute('src')
|
const rawSrc = image.getAttribute('src')
|
||||||
image.src = getImageInfo(rawSrc).src
|
image.src = getImageInfo(rawSrc, this.muya.options).src
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
document.body.appendChild(printContainer)
|
document.body.appendChild(printContainer)
|
||||||
|
@ -35,6 +35,8 @@ const state = {
|
|||||||
textDirection: 'ltr',
|
textDirection: 'ltr',
|
||||||
hideQuickInsertHint: false,
|
hideQuickInsertHint: false,
|
||||||
imageInsertAction: 'folder',
|
imageInsertAction: 'folder',
|
||||||
|
serverFolderPath: '',
|
||||||
|
localFolderPath: '',
|
||||||
imagePreferRelativeDirectory: false,
|
imagePreferRelativeDirectory: false,
|
||||||
imageRelativeDirectoryName: 'assets',
|
imageRelativeDirectoryName: 'assets',
|
||||||
hideLinkPopup: false,
|
hideLinkPopup: false,
|
||||||
@ -137,6 +139,10 @@ const actions = {
|
|||||||
ipcRenderer.send('mt::ask-for-modify-image-folder-path', value)
|
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 }) {
|
SELECT_DEFAULT_DIRECTORY_TO_OPEN ({ commit }) {
|
||||||
ipcRenderer.send('mt::select-default-directory-to-open')
|
ipcRenderer.send('mt::select-default-directory-to-open')
|
||||||
},
|
},
|
||||||
|
@ -79,14 +79,22 @@ export const moveImageToFolder = async (pathname, image, outputDir) => {
|
|||||||
const filename = path.basename(imagePath)
|
const filename = path.basename(imagePath)
|
||||||
const extname = path.extname(imagePath)
|
const extname = path.extname(imagePath)
|
||||||
const noHashPath = path.join(outputDir, filename)
|
const noHashPath = path.join(outputDir, filename)
|
||||||
|
|
||||||
|
// If the file doesn't need to be moved, return it
|
||||||
if (noHashPath === imagePath) {
|
if (noHashPath === imagePath) {
|
||||||
return imagePath
|
return imagePath
|
||||||
}
|
}
|
||||||
const hash = getContentHash(imagePath)
|
|
||||||
// To avoid name conflict.
|
let targetPath = noHashPath
|
||||||
const hashFilePath = path.join(outputDir, `${hash}${extname}`)
|
if (fs.existsSync(targetPath)) {
|
||||||
await fs.copy(imagePath, hashFilePath)
|
// If a file already exists in the target location, use the image's content hash to avoid
|
||||||
return hashFilePath
|
// name conflict.
|
||||||
|
const hash = getContentHash(imagePath)
|
||||||
|
targetPath = path.join(outputDir, `${hash}${extname}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.copy(imagePath, targetPath)
|
||||||
|
return targetPath
|
||||||
} else {
|
} else {
|
||||||
return Promise.resolve(image)
|
return Promise.resolve(image)
|
||||||
}
|
}
|
||||||
|
@ -31,6 +31,8 @@
|
|||||||
"textDirection": "ltr",
|
"textDirection": "ltr",
|
||||||
"hideQuickInsertHint": false,
|
"hideQuickInsertHint": false,
|
||||||
"imageInsertAction": "path",
|
"imageInsertAction": "path",
|
||||||
|
"serverFolderPath": "",
|
||||||
|
"localFolderPath": "",
|
||||||
"imagePreferRelativeDirectory": false,
|
"imagePreferRelativeDirectory": false,
|
||||||
"imageRelativeDirectoryName": "assets",
|
"imageRelativeDirectoryName": "assets",
|
||||||
"hideLinkPopup": false,
|
"hideLinkPopup": false,
|
||||||
|
Loading…
Reference in New Issue
Block a user