fix: milkdown had wrong resolution of images to show

This commit is contained in:
2026-02-14 13:38:59 +01:00
parent 54e6a32874
commit 02b93ff5c5
2 changed files with 153 additions and 0 deletions

View File

@@ -17,6 +17,7 @@ import type { Root, List, ListItem } from 'mdast';
import type { Plugin } from 'unified'; import type { Plugin } from 'unified';
import { visit } from 'unist-util-visit'; import { visit } from 'unist-util-visit';
import { macroPlugin } from '../../plugins/macroPlugin'; import { macroPlugin } from '../../plugins/macroPlugin';
import { imageResolverPlugin } from '../../plugins/imageResolverPlugin';
// Import macros module to register all macro definitions // Import macros module to register all macro definitions
import '../../macros'; import '../../macros';
import './MilkdownEditor.css'; import './MilkdownEditor.css';
@@ -291,6 +292,7 @@ const MilkdownProviderInner: React.FC<MilkdownEditorProps> = ({
}) })
.use(commonmark) .use(commonmark)
.use(gfm) .use(gfm)
.use(imageResolverPlugin)
.use(macroPlugin) .use(macroPlugin)
.use(history) .use(history)
.use(listener) .use(listener)

View File

@@ -0,0 +1,151 @@
/**
* Milkdown Image Resolver Plugin
*
* Resolves relative media paths (like media/2005/01/xxx.JPG) to bds-media://
* protocol URLs at render time, so images display correctly in the Milkdown editor.
*
* The underlying document model keeps the original relative paths, so saved
* markdown files retain portable paths.
*/
import { $prose } from '@milkdown/kit/utils';
import { Plugin, PluginKey } from '@milkdown/kit/prose/state';
import type { NodeView, EditorView } from '@milkdown/kit/prose/view';
import type { Node } from '@milkdown/kit/prose/model';
import { useAppStore } from '../store';
/**
* Resolves a relative media path to a bds-media:// URL
* Matches images by:
* 1. Media ID in the path (e.g., media/2025/01/{id}.jpg)
* 2. Original filename
* 3. Filename pattern
*/
function resolveImageSrc(src: string): string {
// Skip if already using bds-media protocol or external URLs
if (src.startsWith('bds-media://') || src.startsWith('http://') || src.startsWith('https://')) {
return src;
}
// Get media list from store
const media = useAppStore.getState().media;
if (media.length === 0) {
return src;
}
// Build lookup maps for efficient matching
const byId = new Map<string, string>();
const byOriginalName = new Map<string, string>();
const byFilename = new Map<string, string>();
for (const m of media) {
byId.set(m.id, m.id);
byOriginalName.set(m.originalName.toLowerCase(), m.id);
byFilename.set(m.filename.toLowerCase(), m.id);
}
// Extract the filename from the path
const filename = src.split('/').pop() || '';
const filenameWithoutExt = filename.replace(/\.[^.]+$/, '');
const filenameLower = filename.toLowerCase();
// Try to match by:
// 1. UUID in path (the file is named by ID)
if (byId.has(filenameWithoutExt)) {
return `bds-media://${filenameWithoutExt}`;
}
// 2. Filename lookup
if (byFilename.has(filenameLower)) {
return `bds-media://${byFilename.get(filenameLower)}`;
}
// 3. Original name lookup
if (byOriginalName.has(filenameLower)) {
return `bds-media://${byOriginalName.get(filenameLower)}`;
}
// No match found, return original
return src;
}
/**
* Custom node view for image nodes that resolves URLs at render time
*/
class ImageNodeView implements NodeView {
dom: HTMLElement;
img: HTMLImageElement;
constructor(node: Node, _view: EditorView, _getPos: () => number | undefined) {
// Create wrapper span
this.dom = document.createElement('span');
this.dom.className = 'milkdown-image-wrapper';
// Create image element
this.img = document.createElement('img');
this.img.alt = node.attrs.alt || '';
this.img.title = node.attrs.title || '';
// Resolve the src URL
const originalSrc = node.attrs.src || '';
this.img.src = resolveImageSrc(originalSrc);
// Store original src for debugging
this.img.dataset.originalSrc = originalSrc;
this.dom.appendChild(this.img);
}
update(node: Node): boolean {
if (node.type.name !== 'image') {
return false;
}
// Update image attributes
this.img.alt = node.attrs.alt || '';
this.img.title = node.attrs.title || '';
const originalSrc = node.attrs.src || '';
const resolvedSrc = resolveImageSrc(originalSrc);
if (this.img.src !== resolvedSrc) {
this.img.src = resolvedSrc;
this.img.dataset.originalSrc = originalSrc;
}
return true;
}
destroy(): void {
// Cleanup if needed
}
stopEvent(): boolean {
return false;
}
ignoreMutation(): boolean {
return true;
}
}
/**
* Plugin key for the image resolver plugin
*/
const imageResolverPluginKey = new PluginKey('imageResolverPlugin');
/**
* ProseMirror plugin that provides custom image node views
*/
export const imageResolverPlugin = $prose(() => {
return new Plugin({
key: imageResolverPluginKey,
props: {
nodeViews: {
image: (node, view, getPos) => new ImageNodeView(node, view, getPos),
},
},
});
});
export default imageResolverPlugin;