This document provides architecture overview and platform-specific build information for contributors.
mdconv is a multi-platform Markdown converter with shared core logic and platform-specific adapters:
┌─────────────────────────────────────────┐
│ Core Conversion Logic │
│ (src/core/converter.ts) │
│ │
│ • HTML → Markdown via Turndown │
│ • Word heading normalization │
│ • Google Docs cleanup │
│ • Monospace detection → code blocks │
│ • Bidirectional rich text conversion │
└──────────────┬──────────────────────────┘
│
┌───────┴────────┐
│ Adapters │
│ (Interfaces) │
└───────┬────────┘
│
┌──────────┼──────────┐
│ │ │
┌───▼────┐ ┌──▼─────┐ ┌──▼─────┐
│ Chrome │ │Firefox │ │Raycast │
│Platform│ │Platform│ │Platform│
└────────┘ └────────┘ └────────┘
src/core/converter.ts- Main conversion engine using Turndownsrc/core/adapters/- Platform abstraction interfaces:clipboard.ts- Clipboard read/write interfacedom-parser.ts- HTML parsing interface
src/core/env.ts- Centralized environment configurationsrc/core/logging.ts- Debug logging utilities
Each platform lives in src/platforms/<platform>/ with:
- Entry points (popup.ts, background.ts, content-script.ts)
- Platform-specific converter wrapper
- Adapters implementing core interfaces
Location: src/platforms/chrome/
- Popup UI (
popup.ts) - Main interface for manual paste & convert - Context Menu (
background.ts) - Right-click "Copy as Markdown" on selections - Content Script (
content-script.ts) - Extracts HTML from page selections - Adapters (
adapters/) - Use browser clipboard APIs and jsdom for parsing
# Development (watch mode)
npm run dev
# Production build
npm run build
# Create distributable ZIP
npm run build:zipOutput: dist/ directory contains unpacked extension, mdconv-extension.zip for store submission
Static Assets: static/ contains manifest.json, popup HTML/CSS, and icons - copied to dist/ during build
Chrome:
- Navigate to
chrome://extensions - Enable "Developer mode"
- Click "Load unpacked" → select
dist/
Edge:
- Navigate to
edge://extensions - Enable "Developer mode"
- Click "Load unpacked" → select
dist/
Location: src/platforms/firefox/
Firefox implementation uses the proxy adapter pattern - it re-exports Chrome adapters because both use standard Web APIs:
// src/platforms/firefox/adapters/firefox-clipboard.ts
export * from "../../chrome/adapters/chrome-clipboard.js";Why this works: Both Chrome and Firefox support:
- Standard
navigator.clipboardAPI - Standard DOM parsing via jsdom
- Manifest V3 (with minor differences handled at manifest level)
-
Manifest: Uses
static/manifest.firefox.jsonwith Firefox-specific fields:browser_specific_settings.geckofor add-on IDdata_collection_permissions: {required: ["none"]}
-
Browser API Access: Uses
globalThis.browserfallback:const browser = globalThis.browser || chrome;
-
Build Target: ESBuild uses
--target=firefox109(vs chrome115)
# Development
npm run dev:firefox
# Production build
npm run build:firefox
# Create distributable ZIP
npm run build:firefox:zip
# Create source package for Mozilla reviewers
npm run build:firefox:sourceOutputs:
dist-firefox/- Unpacked extensionmdconv-firefox.zip- Store submission packagemdconv-firefox-source.zip- Source code for Mozilla review
See FIREFOX_BUILD.md for detailed build instructions provided to Mozilla reviewers.
Location: src/platforms/raycast/ (source) + raycast/ (extension package)
Raycast's store requires self-contained extensions. A prebuild script copies shared source into raycast/src/ with import path rewriting, making the extension fully self-contained:
Source (monorepo):
├── src/core/ ← Shared conversion logic
├── src/types/ ← Type declarations
├── src/platforms/raycast/
│ ├── convert-clipboard.tsx ← Command entry points
│ ├── raycast-converter.ts ← Raycast-specific converter
│ └── adapters/ ← Platform adapters
└── scripts/prepare-raycast-build.mjs ← Prebuild: copies + rewrites imports
Generated (self-contained):
└── raycast/src/ ← All generated by prebuild
├── core/ ← Copied from src/core/
├── types/ ← Copied from src/types/
├── adapters/ ← Copied with imports rewritten
├── convert-*.tsx ← Copied with imports rewritten
└── raycast-converter.ts ← Copied with imports rewritten
-
Prebuild (
scripts/prepare-raycast-build.mjs):- Copies
src/core/→raycast/src/core/ - Copies
src/types/→raycast/src/types/ - Copies adapters with import rewriting (
../../../core/→../core/) - Copies commands/converter with import rewriting (
../../core/→./core/)
- Copies
-
Build: Raycast CLI (
ray build -e dist) compilesraycast/src/entry points -
Key rule: Never edit
raycast/src/directly — changes are overwritten by prebuild. Edit source insrc/platforms/raycast/instead.
Build scripts:
scripts/prepare-raycast-build.mjs— Copies shared source with import rewritingscripts/sync-version.mjs— Syncs version from rootpackage.json
Self-contained configs:
raycast/package.json— Includes all dependencies (turndown, linkedom, etc.)raycast/tsconfig.json— Only override from Raycast template: adds DOM types for linkedom
# From root (includes prebuild sync)
npm run build:raycast
# Development mode (auto-reload in Raycast)
npm run dev:raycast
# From raycast/ directory
cd raycast
npm run build # prebuild runs automatically
npm run devcd raycast
npm run devOpens extension in Raycast with hot-reload. Search any of the six commands (e.g. "Convert Clipboard to Markdown") to test.
Use the publish script which handles all the monorepo gymnastics automatically:
npm run publish:raycastThis script (scripts/raycast-publish.sh) does the following:
- Runs the prebuild to generate
raycast/src/from shared source - Temporarily un-ignores the generated
src/files (they must be committed for Raycast CI) - Strips the
prebuildscript frompackage.json(parent-relative paths won't exist in the monorepo) - Runs
npx @raycast/api@latest publishto open/update a PR onraycast/extensions - Restores
.gitignoreandpackage.jsonto their original state
macOS:
brew install nodeWindows:
- Download from nodejs.org (LTS version)
- Run installer
- Fix PowerShell execution policy if needed:
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
Verify: node --version (should show v25.x or later)
npm install
npm run raycast:install # If working on Raycast extension# Type checking
npm run typecheck
# Chrome extension
npm run build # Production
npm run build:zip # + create ZIP
npm run dev # Watch mode
# Firefox extension
npm run build:firefox
npm run build:firefox:zip
npm run build:firefox:source
npm run dev:firefox
# Raycast extension
npm run build:raycast
npm run dev:raycastnpm test # Run all testsTests use fixtures in test/ covering:
- Word desktop app HTML
- Word Online HTML
- Google Docs HTML
- Outlook Web HTML
- Image handling scenarios
Chrome Extension:
// In popup DevTools console
localStorage.setItem('mdconv.debugClipboard', 'true');
// Paste again to see raw HTML logged
localStorage.removeItem('mdconv.debugClipboard'); // DisableRaycast Extension:
# Check logs in Raycast development console
npm run dev:raycast
# Raycast shows errors/console.log outputEnvironment Variables:
MDCONV_DEBUG=1- Enable all debug loggingMDCONV_DEBUG_CLIPBOARD=1- Log clipboard contentsMDCONV_DEBUG_INLINE=1- Log HTML→Markdown conversion details
See src/core/env.ts for complete environment variable definitions.
- Use
errorconsistently in catch blocks (nevererr,messageError) - Pattern:
} catch (error) { /* handle */ }
- Add JSDoc comments to all exported functions in
src/core/ - Include purpose, parameters, return value, and key behaviors
- Use centralized
src/core/env.tsfor all environment access - Avoid direct
process.envaccess
- Prefer simple functions over classes when possible
- Regularly check for dead code:
npx ts-unused-exports tsconfig.json
Before committing substantial changes:
- ✅ All catch blocks use
errorconsistently? - ✅ Exported functions have JSDoc documentation?
- ✅ Environment access through
src/core/env.ts? - ✅ Checked for unused exports?
- ✅ Tests pass and builds work for all platforms?
Run: npm run typecheck && npm run build && npm run build:raycast && npm test
- Share 80% of code via proxy adapters
- Only differ in manifest and build targets
- Both use standard Web APIs
- macOS only (Raycast limitation)
- Uses Node.js APIs for clipboard access
- Prebuild script generates
raycast/src/from shared source with import rewriting - Development uses parent source; publishing uses self-contained copy in
raycast/src/
- Create a feature branch
- Make changes, following code quality standards above
- Run quality gate checks
- Update tests if needed
- Commit with clear messages
- For Raycast changes, edit source in
src/platforms/raycast/(notraycast/src/)
- Main README: README.md
- Firefox Build Instructions: FIREFOX_BUILD.md
- Product Requirements: PRD.md