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.
src/chrome/contentscript.js: Runs the periodic 2-second interval check on focused compose elementssrc/common/common-logic.js: Implements detection heuristics, installs send-button interceptors, and renders the warning modalsrc/chrome/backgroundscript.js: Supplies the prompt HTML template via privileged script messaging
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});
});
}
Modal Injection and Keyboard Handling
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:
probablyWritingMarkdownanalyzes HTML content for Markdown syntax patterns before allowing sends - Event interception:
setupForgotToRenderInterceptorscaptures 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →