mirror of
https://github.com/wanglin2/obsidian-simplemindmap.git
synced 2026-07-22 07:11:13 +00:00
387 lines
12 KiB
JavaScript
387 lines
12 KiB
JavaScript
import { TFile } from 'obsidian'
|
|
import { parseMarkdownText } from './metadataAndMarkdown.js'
|
|
import { dataURItoBlob, smmFilePathToFileName } from './utils.js'
|
|
import LZString from 'lz-string'
|
|
import { SMM_VIEW_TYPE } from './constant.js'
|
|
import SmmEditView from '../SmmEditView.js'
|
|
|
|
export default class MarkdownPostProcessor {
|
|
constructor(plugin) {
|
|
this.plugin = plugin
|
|
|
|
this.urlCache = new Map()
|
|
this.imageElements = new Map()
|
|
this.emptyMap = new Map()
|
|
|
|
this.handleFileModify = this.handleFileModify.bind(this)
|
|
this.handleFileDelete = this.handleFileDelete.bind(this)
|
|
this.handleModifySvgName = this.handleModifySvgName.bind(this)
|
|
|
|
plugin.app.vault.on('modify', this.handleFileModify)
|
|
plugin.app.vault.on('delete', this.handleFileDelete)
|
|
plugin.app.vault.on('rename', this.handleModifySvgName)
|
|
}
|
|
|
|
destroy() {
|
|
this.plugin.app.vault.off('modify', this.handleFileModify)
|
|
this.plugin.app.vault.off('delete', this.handleFileDelete)
|
|
this.plugin.app.vault.off('rename', this.handleModifySvgName)
|
|
|
|
this.urlCache.forEach(url => URL.revokeObjectURL(url))
|
|
this.urlCache.clear()
|
|
this.imageElements.clear()
|
|
this.emptyMap.clear()
|
|
}
|
|
|
|
async handleFileModify(file) {
|
|
if (!(file instanceof TFile) || !this.plugin._isSmmFile(file)) return
|
|
|
|
try {
|
|
const noUrlCache = !this.urlCache.has(file.path)
|
|
const hasEmpty = this.emptyMap.has(file.path)
|
|
if (noUrlCache && !hasEmpty) {
|
|
return
|
|
}
|
|
const curIsNoImage = noUrlCache && hasEmpty
|
|
const data = await this.plugin.app.vault.read(file)
|
|
const parsedata = parseMarkdownText(data)
|
|
const newUrl = this._getImgUrl(parsedata.svgdata)
|
|
if (!newUrl) {
|
|
return
|
|
}
|
|
|
|
const oldUrl = this.urlCache.get(file.path)
|
|
if (oldUrl) URL.revokeObjectURL(oldUrl)
|
|
this.urlCache.set(file.path, newUrl)
|
|
|
|
const emptyDiv = this.emptyMap.get(file.path)
|
|
if (emptyDiv) {
|
|
this.emptyMap.delete(file.path)
|
|
}
|
|
|
|
const elements = this.imageElements.get(file.path) || new Set()
|
|
elements.forEach(img => {
|
|
img.src = newUrl
|
|
})
|
|
if (curIsNoImage && emptyDiv) {
|
|
const img = this._createSvgImageElement(newUrl, file.path)
|
|
emptyDiv.replaceWith(img)
|
|
}
|
|
} catch (error) {
|
|
console.error('更新图片失败:', error)
|
|
}
|
|
}
|
|
|
|
async handleFileDelete(file) {
|
|
if (!(file instanceof TFile)) return
|
|
|
|
const url = this.urlCache.get(file.path)
|
|
if (url) {
|
|
URL.revokeObjectURL(url)
|
|
this.urlCache.delete(file.path)
|
|
const imgs = this.imageElements.get(file.path)
|
|
imgs.forEach(img => {
|
|
const parentElement = img.parentElement
|
|
parentElement.empty()
|
|
this._createEmptyDiv(
|
|
parentElement,
|
|
file,
|
|
this.plugin._t('tip.fileDeleted'),
|
|
false
|
|
)
|
|
})
|
|
this.imageElements.delete(file.path)
|
|
}
|
|
const emptyDiv = this.emptyMap.get(file.path)
|
|
if (emptyDiv) {
|
|
emptyDiv.textContent = this.plugin._t('tip.fileDeleted')
|
|
emptyDiv.removeEventListener('dblclick', emptyDiv.dblclickHandler)
|
|
this.emptyMap.delete(file.path)
|
|
}
|
|
|
|
this.deletePreviewSvgFile(file)
|
|
}
|
|
|
|
async deletePreviewSvgFile(file) {
|
|
try {
|
|
const targetFileName = smmFilePathToFileName(file.path, '.svg')
|
|
const { embedImageIsSeparateFileFolder } = this.plugin.settings
|
|
const previewFilePath = embedImageIsSeparateFileFolder
|
|
? `${embedImageIsSeparateFileFolder}/${targetFileName}`
|
|
: targetFileName
|
|
const exist = await this.plugin.app.vault.exists(previewFilePath)
|
|
console.log('删除预览图像文件:', previewFilePath, exist)
|
|
if (exist) {
|
|
await this.plugin.app.vault.adapter.remove(previewFilePath)
|
|
}
|
|
} catch (error) {
|
|
console.error('删除预览图像文件失败:', error)
|
|
}
|
|
}
|
|
|
|
async handleModifySvgName(file, oldName) {
|
|
try {
|
|
oldName = smmFilePathToFileName(oldName, '.svg')
|
|
const { embedImageIsSeparateFileFolder } = this.plugin.settings
|
|
const oldPath = embedImageIsSeparateFileFolder + '/' + oldName
|
|
const exist = await this.plugin.app.vault.exists(oldPath)
|
|
if (exist) {
|
|
const newName = smmFilePathToFileName(file.name, '.svg')
|
|
const newPath = embedImageIsSeparateFileFolder + '/' + newName
|
|
await this.plugin.app.vault.adapter.rename(oldPath, newPath)
|
|
}
|
|
} catch (error) {
|
|
console.error('修改svg文件名失败:', error)
|
|
}
|
|
try {
|
|
const markdownLeafs = this.plugin.app.workspace.getLeavesOfType(
|
|
SMM_VIEW_TYPE
|
|
)
|
|
for (const leaf of markdownLeafs) {
|
|
if (
|
|
leaf.view instanceof SmmEditView &&
|
|
leaf.view.file.path === file.path
|
|
) {
|
|
leaf.view.forceSave(true)
|
|
break
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('保存失败:', error)
|
|
}
|
|
}
|
|
|
|
register() {
|
|
this.plugin.registerMarkdownPostProcessor(
|
|
this._markdownPostProcessor.bind(this)
|
|
)
|
|
}
|
|
|
|
async _markdownPostProcessor(el, ctx) {
|
|
try {
|
|
if (this._isSpecialCard(el)) {
|
|
console.log('特殊卡片处理')
|
|
return this._processSpecialCard(el)
|
|
}
|
|
|
|
const embeddedItems = el.querySelectorAll('.internal-embed')
|
|
if (embeddedItems.length === 0) {
|
|
await this._processEditMode(el, ctx)
|
|
} else {
|
|
await this._processReadingMode(embeddedItems, ctx)
|
|
}
|
|
} catch (error) {
|
|
console.error('Markdown处理失败:', error)
|
|
}
|
|
}
|
|
|
|
_isSpecialCard(el) {
|
|
return (
|
|
el.hasClass('components--DynamicDataView-PageCardCoverPreviewContent') &&
|
|
el.children[0]?.getAttribute('data-heading') === 'metadata' &&
|
|
el.children[2]?.getAttribute('data-heading') === 'svgdata' &&
|
|
el.children[4]?.getAttribute('data-heading') === 'linkdata'
|
|
)
|
|
}
|
|
|
|
_processSpecialCard(el) {
|
|
el.classList.add('smmMindCard')
|
|
const svgdata = el.children[3].children[0].innerHTML
|
|
const url = this._getImgUrl(svgdata)
|
|
|
|
const img = this._createSvgImageElement(url)
|
|
el.replaceChildren(img)
|
|
}
|
|
|
|
_createEmptyDiv(containerEl, file, text, listenerDblclick = true) {
|
|
const emptyDiv = containerEl.createEl('div', { text })
|
|
emptyDiv.style.color = 'var(--text-muted)'
|
|
emptyDiv.style.border = '1px dashed var(--text-muted)'
|
|
emptyDiv.style.padding = '10px'
|
|
emptyDiv.style.borderRadius = '4px'
|
|
emptyDiv.style.textAlign = 'center'
|
|
if (listenerDblclick) {
|
|
emptyDiv.dblclickHandler = () => {
|
|
const newWindow = this.plugin.settings.embedDblClickNewWindow
|
|
this.plugin.app.workspace.openLinkText(file?.path, '', newWindow)
|
|
}
|
|
emptyDiv.addEventListener('dblclick', emptyDiv.dblclickHandler)
|
|
}
|
|
return emptyDiv
|
|
}
|
|
|
|
async _processEditMode(el, ctx) {
|
|
const file = this.plugin.app.vault.getFileByPath(ctx.sourcePath)
|
|
if (!this.plugin._isSmmFile(file)) return
|
|
if (ctx.remainingNestLevel !== undefined && ctx.remainingNestLevel < 4)
|
|
return
|
|
|
|
let containerEl = this._findValidContainer(ctx.containerEl)
|
|
if (!containerEl || this._shouldSkipContainer(containerEl)) return
|
|
|
|
try {
|
|
const data = await this.plugin.app.vault.read(file)
|
|
const img = await this._createImageFromFile(file, data)
|
|
containerEl.empty()
|
|
if (img) {
|
|
containerEl.appendChild(img)
|
|
this._updateImgSize(containerEl, img)
|
|
} else {
|
|
const emptyDiv = this._createEmptyDiv(
|
|
containerEl,
|
|
file,
|
|
this.plugin._t('tip.noPreviewImageInFile')
|
|
)
|
|
this.emptyMap.set(file.path, emptyDiv)
|
|
}
|
|
if (containerEl.hasClass('markdown-embed')) {
|
|
containerEl.classList.remove('markdown-embed', 'inline-embed')
|
|
}
|
|
if (containerEl.hasClass('canvas-node-content')) {
|
|
containerEl.classList.add('smm-canvas-node-content')
|
|
}
|
|
} catch (error) {
|
|
console.error(`处理编辑模式失败: ${file?.path}`, error)
|
|
}
|
|
}
|
|
|
|
async _processReadingMode(embeddedItems, ctx) {
|
|
for (const item of embeddedItems) {
|
|
try {
|
|
const fname = item.getAttribute('src')?.split('#')[0]
|
|
if (!fname) continue
|
|
|
|
const file = this.plugin.app.metadataCache.getFirstLinkpathDest(
|
|
fname,
|
|
ctx.sourcePath
|
|
)
|
|
if (!(file instanceof TFile) || !this.plugin._isSmmFile(file)) continue
|
|
const data = await this.plugin.app.vault.read(file)
|
|
const img = await this._createImageFromFile(file, data)
|
|
this._updateImgSize(item, img)
|
|
if (img) {
|
|
item.parentElement?.replaceChild(img, item)
|
|
}
|
|
} catch (error) {
|
|
console.error('处理嵌入项失败', error)
|
|
}
|
|
}
|
|
}
|
|
|
|
_updateImgSize(containerEl, img) {
|
|
if (
|
|
containerEl.parentNode &&
|
|
containerEl.parentNode.classList.contains('popover')
|
|
) {
|
|
containerEl.classList.add('smm-popover-img-preview-content')
|
|
} else {
|
|
const width = containerEl.getAttribute('width')
|
|
const height = containerEl.getAttribute('height')
|
|
if (width) {
|
|
img.width = width
|
|
}
|
|
if (height) {
|
|
img.height = height
|
|
}
|
|
}
|
|
}
|
|
|
|
async _createImageFromFile(file, data) {
|
|
if (this.urlCache.has(file.path)) {
|
|
const url = this.urlCache.get(file.path)
|
|
return this._createSvgImageElement(url, file.path)
|
|
}
|
|
|
|
const parsedata = parseMarkdownText(data)
|
|
const svgdata = parsedata.svgdata
|
|
if (!svgdata) {
|
|
return null
|
|
}
|
|
const url = this._getImgUrl(svgdata)
|
|
this.urlCache.set(file.path, url)
|
|
const img = this._createSvgImageElement(url, file.path)
|
|
return img
|
|
}
|
|
|
|
_getImgUrl(svgdata) {
|
|
if (!svgdata) return ''
|
|
let url = ''
|
|
if (/^!\[\[/.test(svgdata)) {
|
|
const match = svgdata.match(/!\[\[([^\]]+)\]\]/)
|
|
if (match && match[1]) {
|
|
url = this.plugin.app.vault.adapter.getResourcePath(match[1])
|
|
}
|
|
} else {
|
|
const svgBlob = dataURItoBlob(LZString.decompressFromBase64(svgdata))
|
|
url = URL.createObjectURL(svgBlob)
|
|
}
|
|
return url
|
|
}
|
|
|
|
_createSvgImageElement(url, filePath = '') {
|
|
const img = document.createElement('img')
|
|
img.src = url
|
|
img.draggable = false
|
|
|
|
if (filePath) {
|
|
img.setAttribute('data-smm-file', filePath)
|
|
|
|
img.addEventListener('dblclick', () => {
|
|
const newWindow = this.plugin.settings.embedDblClickNewWindow
|
|
this.plugin.app.workspace.openLinkText(filePath, '', newWindow)
|
|
})
|
|
|
|
if (!this.imageElements.has(filePath)) {
|
|
this.imageElements.set(filePath, new Set())
|
|
}
|
|
this.imageElements.get(filePath).add(img)
|
|
|
|
const observer = new MutationObserver(() => {
|
|
if (!document.contains(img)) {
|
|
this.imageElements.get(filePath)?.delete(img)
|
|
observer.disconnect()
|
|
}
|
|
})
|
|
observer.observe(document.body, { subtree: true, childList: true })
|
|
}
|
|
|
|
return img
|
|
}
|
|
|
|
_findValidContainer(containerEl) {
|
|
const invalidClasses = [
|
|
'dataview',
|
|
'cm-preview-code-block',
|
|
'cm-embed-block'
|
|
]
|
|
|
|
while (containerEl) {
|
|
if (invalidClasses.some(cls => containerEl.classList.contains(cls))) {
|
|
return null
|
|
}
|
|
|
|
const isExcalidraw = containerEl.classList.contains('excalidraw-md-host')
|
|
if (
|
|
containerEl.classList.contains('internal-embed') ||
|
|
containerEl.classList.contains('markdown-embed') ||
|
|
containerEl.classList.contains('markdown-reading-view') ||
|
|
isExcalidraw
|
|
) {
|
|
return containerEl
|
|
}
|
|
|
|
containerEl = containerEl.parentElement
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
_shouldSkipContainer(containerEl) {
|
|
return (
|
|
containerEl.hasAttribute('ready') ||
|
|
['dataview', 'cm-preview-code-block', 'cm-embed-block'].some(cls =>
|
|
containerEl.classList.contains(cls)
|
|
)
|
|
)
|
|
}
|
|
}
|