How Markdown Here Detects and Converts Markdown Content in Email Compose Fields
Markdown Here detects rich-text email compose fields by traversing the DOM from document.activeElement to locate editable containers, then extracts raw markdown via MdhHtmlToText, sends it to a background script that runs the marked parser and highlight.js, and finally injects the rendered HTML back into the editor while preserving the original source in a hidden wrapper for unrendering.
Markdown Here enables users to write emails in Markdown within webmail clients like Gmail and Outlook. According to the adam-p/markdown-here source code, the extension employs a seven-stage pipeline that bridges content scripts, background processes, and DOM manipulation to detect and convert Markdown content in email compose fields.
Stage 1: Locating the Focused Compose Element
In src/common/markdown-here.js (lines 50-98), the findFocusedElem() method traverses upward from document.activeElement to identify the target container. The implementation handles complex scenarios including same-origin iframes and Firefox/Thunderbird-specific quirks. When the traversal yields an <html> node, the code automatically substitutes it with the <body> element (lines 90-95) to ensure a valid editing context.
Stage 2: Verifying Rich-Text Editability
Before attempting conversion, the extension validates that the discovered element supports rich-text editing. The elementCanBeRendered() function in src/common/markdown-here.js (lines 100-108) inspects the contentEditable and designMode flags, returning true only for rich-edit fields such as Gmail compose areas or Outlook web editors. This check prevents the extension from activating on plain-text inputs or read-only elements.
Stage 3: Determining Render vs. Unrender State
The extension must decide whether to render fresh Markdown or revert previously rendered HTML. In src/common/markdown-here.js (lines 64-110), the code searches for existing .markdown-here-wrapper elements using findMarkdownHereWrapper() and findMarkdownHereWrappersInRange(). If wrappers exist within the current selection, the system triggers an unrender operation to restore the original Markdown; otherwise, it proceeds to render the content as HTML.
Stage 4: Extracting Raw Markdown Text
Once rendering is confirmed, src/chrome/contentscript.js (lines 57-68) initiates the extraction process. The requestMarkdownConversion function instantiates an MdhHtmlToText helper that parses the DOM selection and extracts visible text while handling email signatures. The raw Markdown is then transmitted to the privileged background script via Utils.makeRequestToPrivilegedScript with the action parameter set to 'render'.
function requestMarkdownConversion(elem, range, callback) {
var mdhHtmlToText = new MdhHtmlToText.MdhHtmlToText(elem, range);
Utils.makeRequestToPrivilegedScript(
document,
{ action: 'render', mdText: mdhHtmlToText.get() },
function (response) {
var renderedMarkdown = mdhHtmlToText.postprocess(response.html);
callback(renderedMarkdown, response.css);
});
}
Stage 5: Background Markdown-to-HTML Conversion
The heavy lifting occurs in src/chrome/backgroundscript.js (lines 17-28), which receives the render request and processes it through MarkdownRender.markdownRender (implemented in src/common/markdown-render.js). This component orchestrates the marked parser for Markdown processing and highlight.js for syntax highlighting, generating both HTML output and associated CSS styles based on user preferences stored in OptionsStore.
if (request.action === 'render') {
OptionsStore.get(function (prefs) {
responseCallback({
html: MarkdownRender.markdownRender(
request.mdText, prefs, marked, hljs),
css: prefs['main-css'] + prefs['syntax-css']
});
});
return true;
}
Stage 6: Injecting Rendered HTML with Inline Styles
With the converted HTML ready, src/common/markdown-here.js (lines 64-97 and 121-168) executes the injection sequence. The renderMarkdown method constructs a wrapper <div class="markdown-here-wrapper"> containing the rendered content and a hidden "raw-MD holder" <div> that base64-encodes the original Markdown for future unrendering. The replaceRange function swaps the user's selection with this wrapper. To ensure compatibility with email clients like Gmail that strip <style> tags, the makeStylesExplicit utility traverses CSS rules and applies them as inline element styles.
Stage 7: Mutation Monitoring and Cleanup
After injection, a MutationObserver attaches to the wrapper element (lines 98-115 in src/common/markdown-here.js) to monitor for content modifications. This observer flags when users manually edit the rendered HTML, enabling the extension to display warnings during unrender operations if the converted content has been altered.
Script Injection and Permission Architecture
The extension conserves resources by injecting scripts only when necessary. Upon installation, src/chrome/backgroundscript.js creates a context-menu entry and monitors tab loading events. When a page loads, the script checks permissions via ContentPermissions.hasPermission and injects the contents of Injector.CONTENT_SCRIPTS (including contentscript.js). A global flag window.markdownHereInjected prevents double-loading in single-page applications.
// In a content script (e.g., after the user clicks the toolbar button)
chrome.runtime.sendMessage({action: 'button-click'}, function () {
// markdownHere is already loaded by the injector
markdownHere(
document, // the page document
requestMarkdownConversion, // function that sends markdown to background
console.log, // optional logger
function (elem, rendered) { // callback after render/unrender
console.log('Done – rendered?', rendered);
});
});
Summary
- DOM Traversal:
findFocusedElem()insrc/common/markdown-here.jslocates editable containers by walking up fromdocument.activeElement, handling iframes and browser quirks. - Editability Check:
elementCanBeRendered()verifiescontentEditableordesignModeflags before processing. - State Detection: The presence of
.markdown-here-wrapperelements determines whether to render new Markdown or unrender existing HTML. - Secure Processing: Raw Markdown extraction occurs in content scripts, but parsing via marked and highlight.js runs in the privileged background context.
- Style Safety:
makeStylesExplicit()converts CSS to inline styles to prevent email clients from stripping<style>tags. - Reversibility: Base64-encoded original Markdown is preserved in a hidden wrapper div, enabling lossless unrendering.
Frequently Asked Questions
How does Markdown Here identify which email compose field to convert?
Markdown Here uses markdownHere.findFocusedElem() in src/common/markdown-here.js to traverse the DOM from document.activeElement upward, drilling into same-origin iframes and handling Firefox/Thunderbird-specific edge cases. If the traversal reaches an <html> node, it substitutes the <body> element to ensure a valid editing target.
Can Markdown Here convert Markdown in plain-text email fields?
No. The elementCanBeRendered() function explicitly checks for contentEditable or designMode properties in src/common/markdown-here.js (lines 100-108). Only rich-text editors like Gmail Compose or Outlook Web return true, preventing activation on plain-text inputs or non-editable regions.
Where does the actual Markdown parsing happen?
The parsing occurs in the background script (src/chrome/backgroundscript.js) rather than the content script for security. The content script extracts raw text via MdhHtmlToText and sends it via Utils.makeRequestToPrivilegedScript, while the background process executes MarkdownRender.markdownRender() using the marked library and highlight.js for syntax highlighting.
How does the extension preserve the original Markdown after conversion?
During rendering, src/common/markdown-here.js creates a wrapper <div class="markdown-here-wrapper"> containing the rendered HTML and a hidden "raw-MD holder" <div> that stores the base64-encoded original Markdown. A MutationObserver monitors the wrapper to detect manual edits, allowing the extension to restore the original text when unrendering only if the HTML remains unmodified.
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 →