How Markdown Here Handles Selection-Based Markdown Conversion: A Deep Dive into Piecemeal Rendering
Markdown Here converts only the selected text fragment by detecting the user selection via getOperationalRange, checking for existing wrappers with findMarkdownHereWrappersInRange, and rendering the fragment through renderMarkdown while storing the original Markdown in a hidden attribute for later reversion.
The adam-p/markdown-here extension enables users to convert Markdown to formatted HTML within email clients and web forms. Unlike bulk conversion tools, it supports selection-based Markdown conversion (also called piecemeal rendering), allowing precise control over which text fragments get processed.
The Three-Step Process for Selection-Based Markdown Conversion
The core logic resides in src/common/markdown-here.js, where three primary functions handle the conversion pipeline.
Step 1: Detecting the Selection with getOperationalRange
The getOperationalRange function (lines 110-138) determines exactly what content to convert. It performs several critical tasks:
- Reads the browser's current selection using the Selection API
- Expands a collapsed cursor (caret with no highlighted text) to encompass the entire focused element
- Works around the OS X-Chrome right-click bug that returns incorrect range boundaries
- Trims any trailing signature block to prevent accidental conversion of email signatures
// Conceptual usage within the extension
const focusedElem = findFocusedElem(document);
const range = getOperationalRange(focusedElem);
// range now contains the precise DOM Range to convert
Step 2: Finding Existing Wrappers with findMarkdownHereWrappersInRange
Before rendering new content, the extension checks for previously converted fragments using findMarkdownHereWrappersInRange (lines 13-35). This function:
- Walks the DOM subtree contained within the operational range
- Collects any elements previously wrapped by Markdown Here
- Enables the extension to revert or re-render multiple fragments in a single operation
This detection mechanism supports the toggle behavior—if wrappers exist in the selection, the extension unrenders them instead of creating new HTML.
Step 3: Rendering the Fragment with renderMarkdown
When no existing wrappers are found, renderMarkdown (lines 64-94) processes the selected fragment:
- Extracts the Markdown text from the range
- Converts it to HTML using the configured Markdown renderer
- Replaces the range contents with the rendered HTML
- Stores the original Markdown in an invisible
<div>using atitleattribute prefixed withMDH:
// Simplified rendering flow
function renderMarkdown(elem, range, markdownRenderer, callback) {
const markdownText = range.cloneContents().textContent;
markdownRenderer(markdownText, function(renderedHtml, css) {
// Insert HTML and preserve original Markdown for unrendering
const wrapper = document.createElement('div');
wrapper.innerHTML = renderedHtml;
wrapper.setAttribute('title', 'MDH:' + markdownText);
range.deleteContents();
range.insertNode(wrapper);
callback();
});
}
Orchestrating Piecemeal Conversion in markdownHere
The top-level markdownHere function coordinates the selection-based workflow:
// src/common/markdown-here.js (excerpt)
focusedElem = findFocusedElem(document);
range = getOperationalRange(focusedElem); // Step 1
wrappers = findMarkdownHereWrappersInRange(range); // Step 2
if (wrappers.length > 0) {
// Unrender existing Markdown (toggle behavior)
unrenderMarkdown(focusedElem, wrappers);
} else if (range) {
// Convert only the selected fragment
renderMarkdown(focusedElem, range, …);
}
This architecture enables three distinct behaviors:
- Selection mode: When text is highlighted, only that fragment converts
- Element mode: When the cursor is collapsed, the entire focused element converts
- Toggle mode: When selecting previously rendered content, it reverts to Markdown
Practical Code Examples for Selection-Based Rendering
Converting a User Selection
This example demonstrates how the extension handles piecemeal conversion when a user selects specific text:
function convertSelection() {
// document refers to the compose window's DOM
markdownHere(
document,
// markdownRenderer callback
function (elem, range, callback) {
// Extract Markdown from the selection
const md = range.cloneContents().textContent;
// Convert to HTML (using any markdown library)
const html = markdown.toHTML(md);
// Return rendered content
callback(html, ''); // No additional CSS needed
},
console.log, // Optional logger
function (elem, rendered) {
console.log('Selection rendered successfully:', rendered);
}
);
}
Re-rendering After Editing
When a user edits previously converted content and wants to update it:
// User selects a rendered fragment and triggers conversion
markdownHere(document, markdownRenderer, null, (elem, rendered) => {
if (!rendered) {
console.log('Fragment reverted to raw Markdown for editing.');
}
});
In this scenario, findMarkdownHereWrappersInRange detects the existing wrapper, triggering unrenderMarkdown instead of renderMarkdown. This restores the original Markdown text from the MDH: title attribute, allowing the user to edit and re-convert.
Key Source Files for Selection Handling
| File | Role in Selection-Based Conversion |
|---|---|
src/common/markdown-here.js |
Core logic containing getOperationalRange, findMarkdownHereWrappersInRange, renderMarkdown, and the orchestrating markdownHere function. |
src/common/utils.js |
Provides helper utilities like rangeIntersectsNode and walkDOM that enable range-based DOM traversal and intersection detection. |
src/common/vendor/dompurify.min.js |
Sanitizes generated HTML before insertion, ensuring safe rendering of user-provided Markdown within the selected range. |
src/common/jsHtmlToText.js |
Converts HTML back to plain text for signature detection and other text-processing operations within selections. |
Summary
- Selection detection relies on
getOperationalRangeinsrc/common/markdown-here.jsto determine precisely which DOM nodes to convert, handling edge cases like collapsed cursors and email signatures. - Wrapper discovery via
findMarkdownHereWrappersInRangeenables the extension to identify previously rendered content, supporting the toggle behavior that reverts Markdown instead of double-rendering. - Fragment rendering through
renderMarkdownconverts only the selected text while preserving the original Markdown in a hiddentitleattribute, allowing future unrendering. - Orchestration by the
markdownHerefunction coordinates these steps to support piecemeal conversion, full-element conversion, and toggle modes based on the current selection state.
Frequently Asked Questions
How does Markdown Here detect what text to convert?
Markdown Here uses the getOperationalRange function in src/common/markdown-here.js to interface with the browser's Selection API. If the user has highlighted text, it uses that exact range. If the cursor is collapsed (just a blinking caret), it expands the range to encompass the entire focused element. The function also trims email signatures and works around OS X-Chrome right-click bugs to ensure accurate detection.
Can I convert multiple separate selections at once?
Yes, the extension supports converting multiple fragments simultaneously if they reside within the same operational range. The findMarkdownHereWrappersInRange function walks the entire DOM subtree covered by the selection and collects all existing Markdown Here wrappers. This allows the markdownHere orchestrator to either unrender all selected fragments or render new content across the entire selection in a single operation.
What happens if I try to convert text that's already been rendered?
When you select previously rendered text and trigger the conversion, the extension detects existing wrappers through findMarkdownHereWrappersInRange. Instead of rendering again, it calls unrenderMarkdown to restore the original Markdown text. The original Markdown is retrieved from a hidden title attribute prefixed with MDH: that was stored during the initial rendering, effectively toggling the content back to its raw state.
How does the extension handle collapsed cursors?
When getOperationalRange detects that the user has a collapsed cursor (no text highlighted), it automatically expands the selection to include the entire focused element. This behavior ensures that pressing the Markdown Here hotkey without an explicit selection converts the whole compose body rather than doing nothing. The function identifies the focused element using findFocusedElem and creates a range that encompasses all of its content.
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 →