How Markdown Here Uses a MutationObserver to Detect User Modifications

Markdown Here attaches a MutationObserver to the rendered HTML wrapper immediately after conversion to flag any subsequent manual edits, preventing accidental data loss during re-rendering.

The adam-p/markdown-here extension transforms plain-text Markdown into formatted HTML inside email compose windows. Once the HTML is inserted, users often manually tweak fonts, colors, or text directly in the rich editor. To ensure these manual changes are not silently overwritten if the user toggles Markdown rendering again, the extension monitors the DOM for post-render modifications using the native browser MutationObserver API.

Why Detect User Modifications?

When Markdown Here renders content, it wraps the generated HTML in a <div class="markdown-here-wrapper">. If a user edits this rendered view—typing new text, deleting nodes, or formatting existing content—and then attempts to toggle back to Markdown or re-render, the extension must decide whether to preserve the raw Markdown source or the modified HTML. Without detection logic, the extension would overwrite the user's manual edits with the original rendered output. The MutationObserver acts as a tripwire, setting a persistent flag on the wrapper that signals "this content has diverged from the source."

MutationObserver Implementation in src/common/markdown-here.js

The detection logic resides in src/common/markdown-here.js, where the extension creates a MutationObserver inside a setTimeout callback approximately 100 milliseconds after inserting the wrapper. This delay ensures the initial rendering DOM mutations have settled before monitoring begins.

The implementation checks for modern MutationObserver or the legacy WebKitMutationObserver fallback, then attaches the observer exclusively to the wrapper element:

// src/common/markdown-here.js: lines 502-513
wrapper.ownerDocument.defaultView.setTimeout(function addMutationObserver() {
  var SupportedMutationObserver =
        wrapper.ownerDocument.defaultView.MutationObserver ||
        wrapper.ownerDocument.defaultView.WebKitMutationObserver;
  if (typeof(SupportedMutationObserver) !== 'undefined') {
    var observer = new SupportedMutationObserver(function (mutations) {
      // User has changed the rendered content – flag it.
      wrapper.setAttribute('markdown-here-wrapper-content-modified', true);
      // No need to keep watching after the first change.
      observer.disconnect();
    });
    // Watch for any DOM changes within the wrapper.
    observer.observe(wrapper, { childList: true, characterData: true, subtree: true });
  }
}, 100);

Observer Configuration Options

The observer is configured with three specific boolean flags that ensure comprehensive detection of user activity:

  • childList: true – Fires when nodes are added or removed inside the wrapper, such as when the user types new characters or deletes existing elements.
  • characterData: true – Fires when text nodes inside the wrapper are modified, capturing edits within existing paragraphs or headings.
  • subtree: true – Extends monitoring to all descendants of the wrapper, ensuring changes nested deep within the HTML structure are caught.

The Modification Flag

When the observer callback fires—indicating the first detected DOM mutation—it performs two atomic operations:

  1. Sets the attribute markdown-here-wrapper-content-modified="true" on the wrapper element.
  2. Disconnects itself via observer.disconnect() to avoid performance overhead from continuous monitoring.

This attribute serves as a persistent marker that the extension checks later. If present, Markdown Here warns the user that re-rendering will discard their manual HTML edits, preventing silent data loss.

Practical Detection Example

To implement similar logic in your own extension or web application, follow this pattern derived from the markdown-here source:

function attachModificationObserver(wrapper) {
  const Obs = window.MutationObserver || window.WebKitMutationObserver;
  if (!Obs) return; // Graceful degradation for legacy browsers

  const observer = new Obs((mutations) => {
    // Flag the wrapper and stop observing
    wrapper.setAttribute('markdown-here-wrapper-content-modified', true);
    observer.disconnect();
  });

  // Monitor all structural and text changes within the tree
  observer.observe(wrapper, {
    childList: true,
    characterData: true,
    subtree: true
  });
}

// Usage check before destructive operations
function hasUserModified(wrapper) {
  return wrapper.hasAttribute('markdown-here-wrapper-content-modified');
}

Browser Compatibility Considerations

The code explicitly checks for window.MutationObserver and falls back to window.WebKitMutationObserver for older Safari versions. If neither API exists, the extension simply skips detection, meaning users on very old browsers will not receive the modification warning. The 100-millisecond setTimeout delay prevents the observer from firing on the initial DOM insertion mutations caused by the extension itself rather than the user.

Summary

  • Purpose: The MutationObserver detects manual user edits to rendered Markdown HTML to prevent accidental overwrites during re-rendering.
  • Location: Implemented in src/common/markdown-here.js immediately after wrapper insertion.
  • Mechanism: Watches the wrapper with { childList: true, characterData: true, subtree: true } and sets markdown-here-wrapper-content-modified="true" on first mutation.
  • Efficiency: The observer disconnects immediately after the first detected change to minimize performance impact.
  • Fallback: Supports legacy WebKitMutationObserver for older browser compatibility.

Frequently Asked Questions

What specific DOM changes trigger the MutationObserver in Markdown Here?

The observer fires on any childList changes (nodes added or removed), characterData changes (text content modified), or subtree changes (modifications to nested descendants) within the markdown-here-wrapper element. This covers typing, deletion, formatting, and paste operations.

Why does the observer disconnect after detecting the first mutation?

The observer calls disconnect() immediately after setting the modification flag to minimize performance overhead. Since the extension only needs to know whether the content has been touched—not track every individual keystroke—persistent observation is unnecessary and computationally wasteful.

How does the extension check if content has been modified later?

Before re-rendering or converting back to Markdown, the extension queries the wrapper element for the presence of the markdown-here-wrapper-content-modified attribute using hasAttribute(). If found, the extension displays a warning dialog to the user.

What happens if the browser does not support MutationObserver?

If neither MutationObserver nor WebKitMutationObserver is available, the extension silently skips the detection logic. The rendering functionality remains intact, but the user will not receive a warning if they edit the rendered HTML and attempt to re-render, risking potential data loss on those legacy browsers.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →