A cross-browser extension (Chrome & Firefox) that adds Markdown support to Trix editors. Paste Markdown content and convert it to HTML with a single click.
- 🔍 Auto-detection: Automatically detects Trix editors on any webpage
- 📝 Toolbar Integration: Adds a "MD" button directly to the Trix toolbar
- 🎨 Modal Input: Clean modal dialog for pasting Markdown content
- 🔄 Markdown Conversion: Uses the
markedlibrary for accurate Markdown-to-HTML conversion - 🌐 Cross-browser: Works on both Chrome and Firefox
- 🎯 Multi-editor Support: Handles multiple Trix editors on the same page (tracks last focused)
- 📦 Popup Fallback: Browser action popup for pages where toolbar injection fails
- Clone or download this repository
- Install dependencies:
npm install
- Build the extension:
npm run build # Build for all browsers npm run build:chrome # Build for Chrome only npm run build:firefox # Build for Firefox only
- Open
chrome://extensions/ - Enable "Developer mode"
- Click "Load unpacked"
- Select the
dist/chromefolder
- Open
about:debugging#/runtime/this-firefox - Click "Load Temporary Add-on"
- Select any file in the
dist/firefoxfolder
- Navigate to a page with a Trix editor
- Click the MD button in the Trix toolbar (or the extension icon)
- Paste your Markdown content in the modal
- Click "Add to Trix Editor" (or press
Ctrl/Cmd + Enter) - The Markdown is converted to HTML and inserted at the beginning of the editor
The extension consists of several components that work together to detect Trix editors, provide a Markdown input UI, and inject converted HTML into the editor.
┌─────────────────────────────────────────────────────────────────────────┐ │ Browser Extension │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │ │ │ Content Script │ │ Page Context │ │ Popup (Fallback)│ │ │ │ (Isolated) │◄──►│ Script │ │ │ │ │ └────────┬─────────┘ └────────┬─────────┘ └────────┬─────────┘ │ │ │ │ │ │ │ ▼ ▼ │ │ │ ┌──────────────────────────────────────────┐ │ │ │ │ Web Page DOM │◄─────────────┘ │ │ │ ┌─────────────┐ ┌─────────────────┐ │ │ │ │ │ trix-editor │ │ trix-toolbar │ │ │ │ │ │ .editor │ │ [MD Button] │ │ │ │ │ └─────────────┘ └─────────────────┘ │ │ │ └──────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────┘ Browser extensions run content scripts in an isolated world - a separate JavaScript execution context from the page. This means:
- ✅ Content scripts CAN access and modify the DOM
- ❌ Content scripts CANNOT access JavaScript objects created by the page's scripts
Trix Editor attaches its API (.editor property with methods like insertHTML() and setSelectedRange()) to the <trix-editor> DOM element via JavaScript. Since this happens in the page's context, our content script cannot see or use these methods directly.
// This works in the page's console, but NOT in a content script: const editor = document.querySelector('trix-editor'); editor.editor.insertHTML('<strong>Hello</strong>'); // .editor is undefined!To access Trix's JavaScript API, we inject a separate script file (page-context.js) that runs in the page's context:
┌─────────────────────┐ ┌─────────────────────┐ │ Content Script │ │ Page Context │ │ (Isolated World) │ │ (Page's World) │ │ │ │ │ │ 1. Detect editor │ │ │ │ 2. Inject button │ │ │ │ 3. Show modal │ │ │ │ 4. Convert MD→HTML │ │ │ │ │ │ │ │ │ ▼ │ │ │ │ 5. Create bridge │ ──────► │ 6. Listen for │ │ element with │ DOM │ requests │ │ data attrs │ Event │ │ │ │ │ │ │ ▼ │ │ │ │ ◄────── │ 7. Access Trix API │ │ ▼ │ DOM │ editor.editor │ │ 8. Read response │ Event │ .insertHTML() │ │ │ │ │ └─────────────────────┘ └─────────────────────┘ Many websites (like OpenCollective) have strict Content Security Policy (CSP) headers that block inline scripts:
Content-Security-Policy: script-src 'self' ... Our solution uses an external script file loaded via <script src="...">, which is CSP-compliant because:
- The script is loaded from the extension's origin (declared in
web_accessible_resources) - It's not inline JavaScript
The main entry point that orchestrates everything:
- TrixDetector: Finds
<trix-editor>elements and monitors for dynamically added ones usingMutationObserver - ToolbarInjector: Locates the
<trix-toolbar>and injects the "MD" button - MarkdownModal: Creates a Shadow DOM-isolated modal for Markdown input
- MarkdownConverter: Uses
markedlibrary to convert Markdown to HTML
// Simplified flow class TrixMarkdownExtension { init() { // Listen for trix-initialize events (editor ready) document.addEventListener('trix-initialize', (event) => { this.enhanceEditor(event.target); }); } enhanceEditor(editor) { // Inject MD button into toolbar const injector = new ToolbarInjector(editor); injector.inject(() => this.openModal(editor)); } openModal(editor) { this.modal.open((markdown) => { this.insertMarkdown(editor, markdown); }); } async insertMarkdown(editor, markdown) { const html = this.converter.convert(markdown); // Use page context script to access Trix API await injectPageContextScript(); await insertHtmlViaPageContext(html, editorSelector); } }Runs in the page's JavaScript context where it can access Trix's API:
// Listens for requests from content script document.addEventListener('trix-markdown-request', (event) => { const bridge = event.target; const { html, editorSelector } = bridge.dataset; // Now we CAN access editor.editor! const editorElement = document.querySelector(editorSelector); const editor = editorElement.editor; editor.setSelectedRange([0, 0]); editor.insertHTML(html); // Send response back bridge.dispatchEvent(new CustomEvent('trix-markdown-response', { detail: { success: true } })); });Content script and page context script communicate via DOM elements and CustomEvents:
// Content script creates a bridge element const bridge = document.createElement('div'); bridge.dataset.html = '<h1>Hello</h1>'; bridge.dataset.editorSelector = '[data-trix-markdown-id="..."]'; document.documentElement.appendChild(bridge); // Dispatch request bridge.dispatchEvent(new CustomEvent('trix-markdown-request')); // Listen for response bridge.addEventListener('trix-markdown-response', (e) => { console.log(e.detail.success); });If the page context approach fails, we try alternative methods:
- execCommand:
document.execCommand('insertHTML', false, html)- deprecated but widely supported - DOM Insertion: Directly insert HTML nodes into the contenteditable area
- Hidden Input: Update the hidden
<input>element that Trix uses for form submission
The modal uses Shadow DOM to ensure styles don't leak in or out:
const shadowHost = document.createElement('div'); const shadowRoot = shadowHost.attachShadow({ mode: 'closed' }); shadowRoot.innerHTML = ` <style>/* Encapsulated styles */</style> <div class="modal-overlay">...</div> `;This prevents:
- Page styles from affecting the modal appearance
- Modal styles from affecting the page
When multiple Trix editors exist on a page, we track which one was last focused:
editor.addEventListener('trix-focus', () => { this.activeEditor = editor; });This ensures Markdown is inserted into the correct editor.
The extension requires web_accessible_resources to allow the page to load our script:
// Chrome (Manifest v3) { "web_accessible_resources": [{ "resources": ["content/page-context.js"], "matches": ["<all_urls>"] }] } // Firefox (Manifest v2) { "web_accessible_resources": [ "content/page-context.js" ] }trix-editor-markdown-button/ ├── src/ │ ├── background/ │ │ └── service-worker.js # Extension lifecycle management │ ├── content/ │ │ ├── main.js # Main content script entry point │ │ ├── trix-detector.js # Detect Trix editors on page │ │ ├── toolbar-injector.js # Inject MD button into toolbar │ │ ├── modal.js # Shadow DOM modal component │ │ ├── modal-styles.js # Modal CSS (injected into Shadow DOM) │ │ └── page-context.js # Runs in page context for Trix API │ ├── popup/ │ │ ├── popup.html # Fallback popup UI │ │ ├── popup.js # Popup logic │ │ └── popup.css # Popup styles │ ├── lib/ │ │ ├── browser-api.js # Chrome/Firefox API abstraction │ │ └── markdown-converter.js # Markdown to HTML (uses marked) │ └── styles/ │ └── modal.css # Toolbar button styles ├── manifests/ │ ├── chrome/ │ │ └── manifest.json # Manifest v3 for Chrome │ ├── firefox/ │ │ └── manifest.json # Manifest v2 for Firefox (stable) │ └── firefox-v3/ │ └── manifest.json # Manifest v3 for Firefox (experimental) ├── tests/ │ ├── unit/ # Jest unit tests │ │ ├── markdown-converter.test.js │ │ ├── trix-detector.test.js │ │ ├── toolbar-injector.test.js │ │ └── modal.test.js │ ├── integration/ # Puppeteer E2E tests │ │ ├── setup.js │ │ └── extension.test.js │ └── setup.js # Jest setup with browser API mocks ├── icons/ # Extension icons ├── scripts/ │ └── generate-icons.js # Icon generation helper ├── dist/ # Built extensions (gitignored) │ ├── chrome/ │ └── firefox/ ├── package.json ├── webpack.config.js ├── jest.config.js ├── babel.config.js └── README.md npm run build # Build for Chrome and Firefox npm run build:chrome # Build Chrome extension npm run build:firefox # Build Firefox extension (Manifest v2) npm run build:firefox-v3 # Build Firefox extension (Manifest v3) npm run dev:chrome # Build Chrome with watch mode npm run dev:firefox # Build Firefox with watch mode npm test # Run unit tests npm run test:watch # Run tests in watch mode npm run test:coverage # Run tests with coverage report npm run test:integration # Run E2E tests (requires built extension)The extension uses marked with GitHub Flavored Markdown (GFM) enabled:
- Bold (
**text**) - Italic (
*text*) - Links (
[text](url)) - Ordered Lists
- Unordered Lists
-
Blockquotes
- Strikethrough (
~~text~~)
- Code blocks (GFM)
- fenced with triple backticks
- but for some reason, all lines of the code block get smashed into a single line
- effectively this means only single line code blocks work properly
- Headers
### H3- Works- None of the other levels work (
#,##,####,#####,######)
- Images (
) - Tables (GFM)
inline code- Horizontal Rules (
---)
Note: Trix Editor's
insertHTML()method handles sanitization by converting HTML to its internal document model. Any formatting that Trix doesn't support will be automatically filtered out.
| Browser | Manifest | Status |
|---|---|---|
| Chrome 100+ | v3 | ✅ Stable |
| Firefox 109+ | v2 | ✅ Stable |
| Firefox 109+ | v3 |
- Rationale: Fast, well-maintained, small bundle size (~12KB gzip)
- Alternative considered:
markdown-it(more extensible but larger)
- Rationale: Trix Editor's
insertHTML()method converts HTML into its internal document model, automatically filtering out any formatting it cannot represent - Benefit: No redundant sanitization, smaller bundle size, consistent behavior with Trix's native HTML handling
- Rationale: Ensures modal styles don't conflict with host page styles
- Benefit: Consistent appearance across all websites
- Rationale: Content scripts run in an isolated world and cannot access JavaScript properties attached to DOM elements by page scripts
- Solution: Inject a separate script file that runs in the page's context
- CSP Compliance: Uses external script file (not inline) declared in
web_accessible_resources
- Rationale: Handles multiple Trix editors on same page
- Implementation: Listen to
trix-focusevent to track the last-focused editor
var element = document.querySelector("trix-editor"); element.editor.setSelectedRange([0, 0]); // Position at start element.editor.insertHTML("<strong>Hello</strong>");trix-initialize: Editor is ready (use this to enhance editors)trix-focus: Editor received focustrix-blur: Editor lost focustrix-change: Content changed
<trix-toolbar id="toolbar-id"> <div class="trix-button-row"> <span class="trix-button-group"> <!-- Text formatting buttons --> </span> <span class="trix-button-group"> <!-- Block formatting buttons --> </span> </div> </trix-toolbar> <trix-editor toolbar="toolbar-id"></trix-editor>- Fork the repository
- Create a feature branch:
git checkout -b feature/my-feature - Make your changes
- Run tests:
npm test - Commit:
git commit -am 'Add my feature' - Push:
git push origin feature/my-feature - Submit a Pull Request
The project includes 96 unit tests covering all major components:
npm testTest Coverage:
| Component | Tests | Coverage |
|---|---|---|
| MarkdownConverter | 18 | Input validation, MD→HTML conversion |
| TrixDetector | 25 | Editor detection, MutationObserver, focus tracking |
| ToolbarInjector | 20 | Toolbar finding, button injection, events |
| MarkdownModal | 33 | Shadow DOM, open/close, keyboard shortcuts |
A public LICENSE that makes software free for noncommercial and small-business use, with a guarantee that fair, reasonable, and nondiscriminatory paid-license terms will be available for everyone else.
See the Big Time License v2.0.2 for what circumstances require a paid license.
$0.25 USD per employee per year for qualifying "Big Business" commercial use, as defined above. If you're interested in licensing trix-editor-markdown-button for your business, please contact peter@9thbit.net, and join the Official Discord 👉️ .
40 employees = $10 USD per year
Payments are accepted via any of the following:
Request a fair commercial license by sending an email to peter@9thbit.net and messaging the #usr-pboling channel on the Official Discord 👉️ . If both of your contact attempts fail to elicit a response within the time period allotted in Big Business the licensor will consider that equivalent to a fair commercial license under Big Business.
- Trix Editor by Basecamp
- marked for Markdown parsing