Reading and saving
Read Markdown on demand and connect storage or external updates through incremental events.
Read the complete source
Call getMarkdown() when saving, exporting, or submitting. The result is Markdown text with the original file-format policy supported by the editor preserved.
const markdown = editor.getMarkdown()
// Pass markdown to your application's persistence API.
Read Markdown in the example captures a snapshot. After further edits, read it again to obtain the latest text.
Subscribe to incremental changes
onChange supplies documentId, revision, changes, selection, and origin. Use it to mark unsaved changes or connect incremental host storage. Rendering a toolbar does not require reading the entire document on each keystroke.
const unsubscribe = editor.onChange(event => {
console.log('Document revision', event.documentId, event.revision)
})
// Unsubscribe when the observer's lifecycle ends.
unsubscribe()
For autosave, the application schedules writes and handles failures, retries, and submission order. A successful server response should not automatically replace the full editor content.
External updates
applyChanges() accepts the expected document identity and base revision. Offsets must refer to that revision; stale revisions are rejected. After rejection, obtain the latest state and resolve the conflict.
const result = editor.applyChanges({
documentId: editor.documentId,
baseRevision: editor.revision,
changes: { from: 0, to: 0, insert: 'Additional note.\n\n' },
origin: 'host',
})
if (!result.ok) console.warn(result.reason)
Versioned changes alone are not a multi-user collaboration protocol. To switch to a different document, use replaceDocument(markdown, documentId).
A complete serialized autosave adapter
This TypeScript module serializes writes for one document and reads snapshots only when saving. If source changes during a request, it saves the next version after completion. Failures retain dirty state; the next edit or explicit flush() retries. Responses never replace editor text.
import type { AutosaveOptions, AutosaveSession, SaveSnapshot } from "./types";
/** @param options - Host persistence policy and one current document. @returns Serial save ownership for that document only. */
export function attachAutosave(options: AutosaveOptions): AutosaveSession {
const { editor } = options;
const scope = editor.documentScope.fork();
let dirty = false;
let timer: ReturnType<typeof setTimeout> | undefined;
let running: Promise<void> | undefined;
/** Writes successive immutable snapshots; slow responses never replace the open source. */
async function drain(): Promise<void> {
while (dirty && !scope.disposed) {
dirty = false;
const snapshot: SaveSnapshot = { documentId: editor.documentId, revision: editor.revision, markdown: editor.getMarkdown() };
options.status("saving");
try {
await options.save(snapshot, scope.signal);
if (scope.disposed) return;
if (!dirty && editor.revision === snapshot.revision) options.status("saved");
} catch (error) {
if (!scope.disposed) { dirty = true; options.status("error"); options.error(error); }
return; // An explicit flush or the next source edit retries; no busy retry loop.
}
}
}
/** @returns Completion of prior work and any source dirty when this flush can acquire the writer. */
async function flush(): Promise<void> {
clearTimeout(timer);
// Recheck after awaiting: another flush may have acquired the single writer.
while (running) await running;
if (scope.disposed || !dirty) return;
running = drain().finally(() => { running = undefined; });
await running;
}
scope.own(editor.onChange(() => {
dirty = true;
options.status("dirty");
clearTimeout(timer);
timer = setTimeout(() => { void flush(); }, options.delay ?? 600);
}));
scope.own(() => clearTimeout(timer));
return { flush, dispose: () => scope.dispose() };
}
import { attachAutosave } from './recipes/autosave'
const saving = attachAutosave({
editor,
save: async (snapshot, signal) => {
const response = await fetch(`/api/documents/${encodeURIComponent(snapshot.documentId)}`, {
method: 'PUT', signal, headers: { 'Content-Type': 'text/markdown; charset=utf-8' },
body: snapshot.markdown,
})
if (!response.ok) throw new Error(`save-http-${response.status}`)
},
status: value => { console.log('Save status', value) },
error: error => { console.error(error) },
})
// Before leaving, await flush and let the host decide based on save status.
await saving.flush()
The host supplies /api/documents/:id. The adapter starts saving after the first source change; loading a document does not mark it dirty. A settled flush is not proof of success: status/error report failures, which require retained feedback and retry controls.
Server concurrency and offline use
Local revision is not a server version. Multiple clients need server ETags, versions or a collaboration protocol. Handle 409/412 with merge or conflict UI instead of retrying an overwrite indefinitely.
Offline storage belongs to the host and can retain exact Markdown plus server version. Do not display server-saved status before durable acknowledgement. Arbitrary asynchronous requests cannot be assumed to complete during beforeunload.
Replacement and external echoes
Resolve unsaved state, end the old save session, replace the document, then create a fresh session. The old document scope cancels in-flight resources.
For same-document external edits, applyChanges() offsets belong to the logical text at baseRevision. Success advances revision. Mark origins and suppress same-source echoes in host listeners. On stale-target, remap or merge; do not merely substitute the latest baseRevision while keeping old offsets.
File responsibilities
The editor receives decoded strings. Byte encoding, file BOM handling, names, access control, attachments and downloads belong to the application. Use getMarkdown() for serialized text and state.doc for editing offsets. Rendered HTML and component state are not recoverable Markdown archives.