How the Markdown Here "Forgot-to-Render" Detection and Warning System Works

Markdown Here uses a periodic polling mechanism combined with heuristic pattern matching to detect raw Markdown in email compose windows, intercepting send actions to display a modal warning when users attempt to send unrendered content.

The forgot-to-render detection system in the adam-p/markdown-here repository prevents users from accidentally sending raw Markdown syntax instead of formatted HTML. This feature monitors supported webmail clients like Gmail and Fastmail, analyzing compose window content every two seconds to identify unrendered Markdown before interception occurs.

Architecture Overview

The system distributes responsibilities across three core modules to respect browser security models while maintaining clean separation of concerns.

The Periodic Detection Loop

The content script initiates monitoring by loading user preferences and scheduling regular checks.

// src/chrome/contentscript.js
let forgotToRenderIntervalCheckPrefs = null;

// Load preferences including the "forgot-to-render" toggle
Utils.makeRequestToPrivilegedScript(document,
                                   {action: 'get-options'},
                                   function(prefs){ 
                                     forgotToRenderIntervalCheckPrefs = prefs; 
                                   });

function forgotToRenderCheck() {
  if (!forgotToRenderIntervalCheckPrefs ||
      !forgotToRenderIntervalCheckPrefs['forgot-to-render-check-enabled-2']) {
    return;
  }

  let focusedElem = markdownHere.findFocusedElem(window.document);
  if (!focusedElem) return;

  // Delegate detection to CommonLogic
  CommonLogic.forgotToRenderIntervalCheck(
        focusedElem, markdownHere, MdhHtmlToText, marked,
        forgotToRenderIntervalCheckPrefs);
}

setInterval(forgotToRenderCheck, 2000);

The interval only executes when the user enables forgot-to-render-check-enabled-2 in the extension options.

Heuristic Markdown Detection

The CommonLogic.forgotToRenderIntervalCheck function in src/common/common-logic.js orchestrates the detection pipeline.

// src/common/common-logic.js
function forgotToRenderIntervalCheck(focusedElem, MarkdownHere, MdhHtmlToText, marked, prefs) {
  if (!prefs['forgot-to-render-check-enabled-2']) return;

  // 1. Find the Send button selector for the current webmail client
  var forgotToRenderButtonSelector = getForgotToRenderButtonSelector(focusedElem);
  if (!forgotToRenderButtonSelector) return;

  // 2. Verify the element can be rendered
  if (!MarkdownHere.elementCanBeRendered(focusedElem)) return;

  // 3. Install interceptors once per compose element
  if (typeof(focusedElem[WATCHED_PROPERTY]) === 'undefined') {
    setupForgotToRenderInterceptors(focusedElem, MdhHtmlToText, marked, prefs);
    focusedElem[WATCHED_PROPERTY] = true;
  }
}

Pattern Matching Logic

The probablyWritingMarkdown function scans the raw HTML content for typical Markdown syntax patterns.

  • Bullet lists (- , * , 1. )

  • Code backticks (` or ``)

  • Math delimiters ($...$)

  • Header markers (# )

  • Emphasis syntax (**bold**, *italic*)

  • Link syntax ([text](url))

If any pattern matches, the system flags the content as potentially unrendered Markdown requiring user confirmation.

Intercepting the Send Action

Once detection confirms the environment supports interception, setupForgotToRenderInterceptors attaches event listeners to capture send attempts before the webmail client processes them.

// src/common/common-logic.js
function setupForgotToRenderInterceptors(composeElem, MdhHtmlToText, marked, prefs) {
  var composeSendButton = findClosestSendButton(composeElem);
  
  // Determine if content looks like Markdown
  var shouldIntercept = function() {
    var mdMaybe = new MdhHtmlToText.MdhHtmlToText(composeElem, null, true).get();
    return probablyWritingMarkdown(mdMaybe, marked, prefs);
  };

  // Attach listeners to the Send button's parent container
  composeSendButton.parentNode.addEventListener('keydown', composeSendButtonKeyListener, true);
  composeSendButton.parentNode.addEventListener('keyup', composeSendButtonKeyListener, true);
  composeSendButton.parentNode.addEventListener('click', composeSendButtonClickListener, true);
  
  // Capture Ctrl/Cmd+Enter hotkeys on the compose area
  composeElem.parentNode.addEventListener('keydown', sendHotkeyKeydownListener, true);
}

The listeners call eatEvent(event) to stop propagation of the original send command, then invoke showForgotToRenderPromptAndRespond to present the user with a choice.

Displaying the Warning Prompt

Because content scripts cannot access local files directly due to Chrome's Content Security Policy, the system requests the modal HTML from the privileged background script.

// src/common/common-logic.js
function showForgotToRenderPromptAndRespond(composeElem, composeSendButton) {
  Utils.makeRequestToPrivilegedScript(
    composeElem.ownerDocument,
    {action: 'get-forgot-to-render-prompt'},
    function(response){
      showHTMLForgotToRenderPrompt(response.html, composeElem,
                                   composeSendButton, sendOrGoBackToCompose);
    });
}

The background script loads src/common/forgot-to-render-prompt.html, injects localized strings via Utils.getMessage, and returns the final markup:

// src/chrome/backgroundscript.js
else if (request.action === 'get-forgot-to-render-prompt') {
  CommonLogic.getForgotToRenderPromptContent(function(html){
    responseCallback({html: html});
  });
}

The showHTMLForgotToRenderPrompt function injects the HTML into the page body, manages focus, and implements custom keyboard navigation.

  • Escape key: Cancels the send and returns to the compose window
  • Tab key: Cycles focus between the "Back to Safety" and "Send Anyway" buttons
  • Enter/Space: Activates the focused button (with debouncing to prevent double-triggering)

When the user selects Send Anyway, the callback executes Utils.fireMouseClick(composeSendButton), programmatically triggering the original Gmail send flow. Selecting Back to Safety refocuses the compose element without sending.

Enabling and Debugging the Feature

Users activate the detection system via the options page checkbox controlling the forgot-to-render-check-enabled-2 preference.

Developers can manually trigger the detection pipeline for debugging:

// Console execution in Gmail compose window
CommonLogic.forgotToRenderIntervalCheck(
    markdownHere.findFocusedElem(document),
    markdownHere,
    MdhHtmlToText,
    marked,
    { 'forgot-to-render-check-enabled-2': true }
);

Summary

  • Periodic polling: The content script checks focused compose elements every 2 seconds via setInterval
  • Heuristic detection: probablyWritingMarkdown analyzes HTML content for Markdown syntax patterns before allowing sends
  • Event interception: setupForgotToRenderInterceptors captures clicks and keyboard shortcuts (Ctrl/Cmd+Enter) on the Send button
  • Privileged messaging: The background script supplies the modal HTML template because content scripts cannot read local files directly
  • User choice: The modal offers "Back to Safety" (refocus compose) or "Send Anyway" (fire original click event)

Frequently Asked Questions

How does Markdown Here detect that I'm writing Markdown and not just plain text?

The extension uses the probablyWritingMarkdown function in src/common/common-logic.js to scan the compose element's HTML for specific syntax patterns including bullet lists, backticks for code, header markers, emphasis asterisks, and link brackets. If any Markdown-specific patterns are detected, the system assumes you intended to render the content before sending.

What happens if I disable the forgot-to-render check in the options?

When the forgot-to-render-check-enabled-2 preference is set to false, the forgotToRenderCheck function in src/chrome/contentscript.js returns immediately without calling CommonLogic.forgotToRenderIntervalCheck. No interceptors are installed on the Send button, and the email client processes send commands normally without displaying the warning modal.

Why does the extension need to communicate with the background script just to show a warning dialog?

Chrome's extension architecture restricts content scripts from reading local HTML files directly due to Content Security Policy limitations. The get-forgot-to-render-prompt action in src/chrome/backgroundscript.js reads src/common/forgot-to-render-prompt.html, processes localization placeholders like {{forgot_to_render_prompt_title}}, and returns the final HTML string to the content script for injection into the page DOM.

Can the forgot-to-render detection work with keyboard shortcuts like Ctrl+Enter?

Yes. The setupForgotToRenderInterceptors function explicitly attaches a sendHotkeyKeydownListener to the compose element's parent node to capture Ctrl+Enter (Windows/Linux) or Cmd+Enter (macOS) combinations. This ensures the warning appears whether you click the Send button or use keyboard shortcuts to send the email.

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 →