Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 20 additions & 4 deletions docs/IMAGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

**Prefer relative assets folder:**
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

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.
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:**

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:

Expand All @@ -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

Expand Down
5 changes: 5 additions & 0 deletions src/main/app/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
})
Expand Down
20 changes: 20 additions & 0 deletions src/main/dataCenter/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
Expand Down
10 changes: 10 additions & 0 deletions src/main/preferences/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 2 additions & 2 deletions src/muya/lib/contentState/dragDropCtrl.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/muya/lib/contentState/formatCtrl.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
6 changes: 3 additions & 3 deletions src/muya/lib/contentState/pasteCtrl.js
Original file line number Diff line number Diff line change
Expand Up @@ -150,15 +150,15 @@ const pasteCtrl = ContentState => {
return null
}

const { src } = getImageSrc(imagePath)
const { src } = getImageSrc(imagePath, 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, {
src: newSrc
})
Expand Down Expand Up @@ -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
})
Expand Down
6 changes: 3 additions & 3 deletions src/muya/lib/eventHandler/clickEvent.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion src/muya/lib/eventHandler/keyboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion src/muya/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion src/muya/lib/parser/render/renderBlock/renderLeafBlock.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
2 changes: 1 addition & 1 deletion src/muya/lib/parser/render/renderInlines/image.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
2 changes: 1 addition & 1 deletion src/muya/lib/parser/render/renderInlines/referenceImage.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
65 changes: 61 additions & 4 deletions src/muya/lib/ui/imageSelector/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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 (
Expand Down Expand Up @@ -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
Expand All @@ -255,23 +282,22 @@ 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,
title
})
}
} 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()
Expand Down Expand Up @@ -302,6 +328,9 @@ class ImageSelector extends BaseFloat {
}, {
label: 'Embed link',
value: 'link'
}, {
label: 'Rename',
value: 'rename'
}]

if (this.unsplash) {
Expand Down Expand Up @@ -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: {
Expand Down
2 changes: 1 addition & 1 deletion src/muya/lib/utils/importMarkdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
Loading