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