# How Markdown Here Handles Selection-Based Markdown Conversion: A Deep Dive into Piecemeal Rendering

> Discover how Markdown Here achieves selection based Markdown conversion with piecemeal rendering. Learn about fragment detection, rendering, and reversion.

- Repository: [Adam Pritchard/markdown-here](https://github.com/adam-p/markdown-here)
- Tags: deep-dive
- Published: 2026-03-05

---

**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`](https://github.com/adam-p/markdown-here/blob/main/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

```javascript
// 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:

1. Extracts the Markdown text from the range
2. Converts it to HTML using the configured Markdown renderer
3. Replaces the range contents with the rendered HTML
4. Stores the original Markdown in an invisible `<div>` using a `title` attribute prefixed with `MDH:`

```javascript
// 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:

```javascript
// 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:

```javascript
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:

```javascript
// 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`](https://github.com/adam-p/markdown-here/blob/main/src/common/markdown-here.js)** | Core logic containing `getOperationalRange`, `findMarkdownHereWrappersInRange`, `renderMarkdown`, and the orchestrating `markdownHere` function. |
| **[`src/common/utils.js`](https://github.com/adam-p/markdown-here/blob/main/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`](https://github.com/adam-p/markdown-here/blob/main/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`](https://github.com/adam-p/markdown-here/blob/main/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 `getOperationalRange` in [`src/common/markdown-here.js`](https://github.com/adam-p/markdown-here/blob/main/src/common/markdown-here.js) to determine precisely which DOM nodes to convert, handling edge cases like collapsed cursors and email signatures.
- **Wrapper discovery** via `findMarkdownHereWrappersInRange` enables the extension to identify previously rendered content, supporting the toggle behavior that reverts Markdown instead of double-rendering.
- **Fragment rendering** through `renderMarkdown` converts only the selected text while preserving the original Markdown in a hidden `title` attribute, allowing future unrendering.
- **Orchestration** by the `markdownHere` function 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`](https://github.com/adam-p/markdown-here/blob/main/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.