Skip to content

Images

The editor shows Markdown images (![alt](src)) in place, lets the user paste or drop new ones, and resizes them with a drag. Where image bytes come from and where they go is the host's decision, through two EditorHost callbacks.

resolveImage(src) gets the image target exactly as written and returns a URL the page may load, or null to show the source text instead.

import { createEditorState } from "@openmarkdown-dev/editor";
const state = createEditorState({
doc,
docPath: "/notes/today.md",
host: {
resolveImage: (src) => {
if (src.startsWith("https://")) return src;
return `/files/notes/${src.replace(/^\.\//, "")}`; // your server serves the document's folder
},
},
});
  • Without resolveImage, absolute https: images load, from any host, and nothing else does. The browser fetches them when the document is shown, which tells their server the document was opened. For private documents, pass a resolveImage that allows only your own hosts, or turn remote images off entirely:

    createEditorState({ doc, host: { resolveImage: () => null } }); // no image loads; the source shows
  • Whatever you return, script-capable URLs (javascript: and similar) are dropped. See Untrusted content.

When the user pastes or drops an image, the editor calls saveAttachment(docPath, bytes, hint) with the image bytes and a file-name hint. Store the bytes and resolve the document-relative path to link; the editor inserts an image link to that path where the image was pasted or dropped, with the caret in its alt text. Resolve null when the save failed; the editor inserts nothing (report the failure yourself).

import { createEditorState } from "@openmarkdown-dev/editor";
const urls = new Map<string, string>();
let n = 0;
const state = createEditorState({
doc: "",
docPath: "/draft.md", // paste and drop need a document path
host: {
saveAttachment: async (_docPath, bytes, hint) => {
const name = `attachments/pasted-${++n}.png`;
urls.set(name, URL.createObjectURL(new Blob([bytes.slice()], { type: "image/png" })));
return name;
},
resolveImage: (src) => urls.get(src) ?? (src.startsWith("https://") ? src : null),
},
});

This keeps images in the browser tab only. A real app uploads the bytes and returns the path or URL it stored them under.

resolveImage may be called more than once for the same image, so keep it free of side effects. When its answer changes, for example after the reader allows remote images, call editor.refreshImages() and every image is resolved again, including those in tables.

  • Without saveAttachment, pasting or dropping an image inserts nothing.
  • With docPath set to null (an unsaved document), there is nowhere to store beside, so the editor does not call saveAttachment.
  • In both cases the editor stays silent. To tell the user, pass onUnsupportedPaste: it is called with the reason ("no-save-attachment" or "no-doc-path") and how many images were dropped.
import { createEditorState } from "@openmarkdown-dev/editor";
const state = createEditorState({
doc,
host: {
onUnsupportedPaste: (reason, count) => {
showToast(`${count} image(s) not added: this editor cannot store images.`);
},
},
});
  • hint is the file's name, or its MIME type when it has no name. It is a hint, not a checked type: look at the bytes before you store or serve them.

Every image has a grip in its corner. Dragging it resizes the image, and on release the editor writes the width into the alt text as |W:

![diagram|320](diagram.png)

Only that width changes in the file. In reading mode the grip is hidden.