# Markdown Here — Administrator & Developer Guide

## 1. Architecture Overview

Markdown Here is a browser extension (WebExtension API, Manifest V3) for Chrome and Firefox. It renders Markdown typed in rich-text `contenteditable` fields into styled HTML in-place, without any server component. All rendering happens synchronously inside the browser using a bundled fork of marked.js and highlight.js. Settings are persisted via `chrome.storage.sync`.

```
User action (Alt+Shift+M / toolbar / context menu)
         │
         ▼
  backgroundscript.js (service worker / background page)
         │  handleActionClick()
         │  ├── Injector.injectScripts(tabId)  ← injects content scripts on demand
         │  └── chrome.tabs.sendMessage({action: 'button-click'})
         │
         ▼
  contentscript.js (injected into active tab)
         │  requestHandler() → markdownHere(document, requestMarkdownConversion, ...)
         │
         ▼
  markdown-here.js  (core toggle logic)
         │  ├── findFocusedElem()   — drill into iframes
         │  ├── elementCanBeRendered()
         │  ├── getOperationalRange()   — selection or full element
         │  ├── findMarkdownHereWrapper()   — detect existing render → revert path
         │  └── render path:
         │       requestMarkdownConversion(elem, range, callback)
         │              │
         │              ▼
         │       MdhHtmlToText.get()  — extract plain text from HTML field
         │       Utils.makeRequestToPrivilegedScript({action:'render', mdText})
         │              │
         │              ▼
         │       backgroundscript.js: MarkdownRender.markdownRender(mdText, prefs, marked, hljs)
         │       → marked(mdText, markedOptions) + optional mathify()
         │       → returns {html, css}
         │              │
         │              ▼
         │       mdhHtmlToText.postprocess(html)
         │       makeStylesExplicit(wrapperElem, css)  — inline CSS rules
         │       replaceRange(range, html)   — write rendered HTML into DOM
```

> [!NOTE]
> Rendering is performed in the **background script** (service worker in Chrome MV3, background page in Firefox MV2/MV3), not in the content script. This architectural choice prevents third-party page scripts from accessing the rendering pipeline. The content script communicates via `chrome.runtime.sendMessage`, and only the sanitized HTML result is returned.

### Component inventory

| Component | Path | Role |
|-----------|------|------|
| Manifest | `src/manifest.json` | Extension declaration, permissions, keyboard commands |
| Background script | `src/chrome/backgroundscript.js` | Service worker / background page; handles all rendering |
| Content script | `src/chrome/contentscript.js` | Injected into pages; handles toggle events, forgot-to-render interval |
| Core toggle logic | `src/common/markdown-here.js` | Render/revert state machine, DOM manipulation |
| Markdown renderer | `src/common/markdown-render.js` | marked.js wrapper with MDH customizations |
| marked.js fork | `src/common/marked.js` | Bundled Markdown parser (GFM + math extension) |
| highlight.js | `src/common/highlightjs/highlight.js` | Syntax highlighting engine |
| Highlight themes | `src/common/highlightjs/styles/` | 100+ CSS theme files |
| Options store | `src/common/options-store.js` | Chrome storage abstraction; defaults; chunked-string handling |
| Options UI | `src/common/options.js` / `options.html` | Settings page logic and markup |
| Options iframe | `src/common/options-iframe.js` / `.html` | Live preview iframe embedded in Options |
| Common logic | `src/common/common-logic.js` | Forgot-to-render detection and prompt logic |
| HTML-to-text | `src/common/mdh-html-to-text.js` | Extracts plain text from contenteditable HTML for rendering |
| HTML-to-text (base) | `src/common/jsHtmlToText.js` | Google-derived HTML-to-text utility |
| Content permissions | `src/common/content-permissions.js` | Manages optional host permissions (chrome.permissions API) |
| Upgrade popup | `src/chrome/upgrade-notification-popup.js` / `.html` | Post-update notification shown in toolbar |
| Utils | `src/common/utils.js` | Shared helpers: messaging, DOM safety, locale strings |
| Default CSS | `src/common/default.css` | Built-in stylesheet for rendered Markdown |
| Locales | `src/_locales/` | i18n message files (13 languages) |
| Build script | `utils/build.js` | Node.js script producing `dist/chrome.zip` and `dist/firefox.zip` |

### Technology stack

| Layer | Technology |
|-------|-----------|
| Extension API | WebExtension (Manifest V3 for Chrome; MV3 / MV2 hybrid for Firefox) |
| Markdown parsing | Custom fork of marked.js (bundled; GFM + math extension) |
| Syntax highlighting | highlight.js (bundled) |
| HTML sanitization | DOMPurify (bundled, `src/common/vendor/dompurify.min.js`) |
| Storage | `chrome.storage.sync` (falls back to `localStorage`) |
| Build tooling | Node.js, npm, `archiver`, `file`, `glob` |
| CSS | Plain CSS (no preprocessor) |
| i18n | WebExtension `_locales/` + `chrome.i18n.getMessage()` |

---

## 2. System Requirements

### Runtime (users)

| Requirement | Detail |
|-------------|--------|
| Browser | Chrome (desktop) or Firefox (desktop) |
| Internet | Optional — required only for LaTeX math rendering (codecogs.com) |
| Permissions granted | `activeTab`, `contextMenus`, `storage`, `scripting` |
| Optional permissions | `http://*/*`, `https://*/*` (needed for forgot-to-render on arbitrary sites) |

### Development / build

| Requirement | Detail |
|-------------|--------|
| Node.js | Any modern LTS version |
| npm | Included with Node.js |
| OS | Any (macOS, Windows, Linux) |

---

## 3. Configuration Reference

All settings are stored in `chrome.storage.sync`. They are managed through the Options page UI and read by both the background script and the content script (via messaging).

### Options keys and defaults

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `main-css` | string (CSS) | Content of `default.css` | Full CSS stylesheet applied to rendered Markdown output. |
| `syntax-css` | string (CSS) | Content of `highlightjs/styles/github.css` | CSS theme for code block syntax highlighting. |
| `math-enabled` | boolean | `false` | Enable LaTeX math rendering via codecogs.com. |
| `math-value` | string (URL template) | See below | URL template for rendering math expressions. |
| `forgot-to-render-check-enabled-2` | boolean | `false` | Prompt user before sending if unrendered Markdown detected. |
| `header-anchors-enabled` | boolean | `false` | Add `id` anchors to headings for in-page linking. |
| `gfm-line-breaks-enabled` | boolean | `true` | Single newlines become `<br>`. |

**Default `math-value` template:**

```text
<img src="https://latex.codecogs.com/png.image?\dpi{120}\inline&space;{urlmathcode}" alt="{mathcode}">
```

Where `{urlmathcode}` is the URL-encoded LaTeX expression and `{mathcode}` is the raw LaTeX (for the alt text).

> [!IMPORTANT]
> The `main-css` and `syntax-css` values are loaded from local extension files on first access (using `__defaultFromFile__` markers in `OptionsStore.defaults`). They are fetched once and stored, so the options object always contains the full CSS text, not a file reference. The chunked storage system (`##0`, `##1` key suffixes) handles values that exceed `chrome.storage.sync.QUOTA_BYTES_PER_ITEM`.

### Storage chunking

Long string values (the CSS fields) exceed `chrome.storage.sync.QUOTA_BYTES_PER_ITEM` (8192 bytes). `OptionsStore` automatically splits strings into numbered chunks using `##` as a separator (e.g., `main-css##0`, `main-css##1`) and recombines them on read.

### Permissions model

The extension uses **optional host permissions** (`http://*/*`, `https://*/*`). These are not granted at install time. They are requested dynamically by `ContentPermissions.requestPermission()` when:
- The forgot-to-render feature is enabled (requests permission for the specific domain)
- The user activates the extension on a page where the auto-inject path needs confirmation

The `forgot-to-render` feature specifically targets `mail.google.com` (Gmail) and `fastmail.com`. The button-click path uses `activeTab` permission, which is always granted, and injects scripts on demand without requiring broad host permissions.

---

## 4. Deployment & Build

### Building from source

```bash
# Clone the repository
git clone https://github.com/adam-p/markdown-here.git
cd markdown-here/utils

# Install build dependencies
npm install

# Build both extension packages
node build.js
```

```text
dist/chrome.zip      ← Load in Chrome via chrome://extensions → Load unpacked (extract first)
dist/firefox.zip     ← Submit to AMO or load via about:debugging
```

> [!NOTE]
> The build script strips Firefox-specific fields from `manifest.json` when producing the Chrome package. Specifically the `"browser_specific_settings"` block with the Firefox gecko `id` is removed for Chrome builds. Both packages share the same `src/` source — there is no platform-specific code other than the background page / service worker dual-path in `backgroundscript.js`.

### Loading the unpacked extension (development)

=== "Chrome"
    1. Extract `dist/chrome.zip` (or point directly at `src/`).
    2. Navigate to `chrome://extensions`.
    3. Enable **Developer mode** (toggle top right).
    4. Click **Load unpacked** and select the directory.

=== "Firefox"
    1. Navigate to `about:debugging#/runtime/this-firefox`.
    2. Click **Load Temporary Add-on**.
    3. Select `dist/firefox.zip` or `src/manifest.json`.

### Content script injection order

The background script injects content scripts in this exact order when a tab is activated:

```text
1. /common/vendor/dompurify.min.js
2. /common/utils.js
3. /common/common-logic.js
4. /common/jsHtmlToText.js
5. /common/marked.js
6. /common/mdh-html-to-text.js
7. /common/markdown-here.js
8. /chrome/contentscript.js
```

The `Injector.checkAndMarkInjected()` function uses `window.markdownHereInjected` as a per-tab flag to prevent double-injection.

---

## 5. Operations

### Service worker lifecycle (Chrome MV3)

In Chrome, the background script runs as a **service worker**. Service workers are torn down after ~30 seconds of inactivity and revived on new extension events. This means:

- Global variables **cannot** store state between activations.
- The `backgroundPage` boolean (set from the URL hash) determines whether the script is running as a legacy background page (Firefox) or a service worker (Chrome MV3).
- When running as a service worker, scripts must be loaded via `importScripts()` on each activation.

> [!WARNING]
> Do not store per-tab state in global variables in `backgroundscript.js`. The service worker may be torn down and restarted between the inject call and the message handler call for a given tab interaction.

### Background page vs. service worker

| Mode | Browser | Lifetime | Script loading |
|------|---------|----------|---------------|
| Service worker | Chrome MV3 | ~30s idle timeout | `importScripts()` on each load |
| Background page | Firefox MV2/MV3 | Session lifetime | Scripts declared in `background.page` HTML |

The `background.html` page is used for the background-page path; `backgroundscript.js` checks `!!location.hash` at startup to detect which mode it is in.

### Auto-injection on tab load

The `chrome.tabs.onUpdated` listener auto-injects content scripts into tabs that have already been granted host permission (via `ContentPermissions.hasPermission(tab.url)`). This allows the forgot-to-render check to run before the user activates the extension — it polls every 2 seconds via `setInterval(forgotToRenderCheck, 2000)`.

### Context menu registration

The context menu item is created once via `chrome.contextMenus.create()` inside `chrome.runtime.onInstalled`. It is only created on `install` or `update` events to avoid duplicate entries.

---

## 6. Security

### Input sanitization

All HTML produced by marked.js rendering passes through **DOMPurify** before being inserted into the DOM. The content script calls `Utils.saferSetOuterHTML()` and `Utils.safelyParseHTML()`, both of which use DOMPurify internally to strip dangerous attributes and elements.

> [!CAUTION]
> An XSS vulnerability was reported in the bundled marked.js and rendering pipeline (see [#978](https://github.com/adam-p/markdown-here/issues/978)). The DOMPurify integration is the primary mitigation. Ensure the vendored `dompurify.min.js` is kept up to date when building from source.

### Rendering pipeline isolation

Rendering is intentionally executed in the **privileged background script**, not in the content script. The content script sends raw Markdown text to the background via `chrome.runtime.sendMessage` and receives only the sanitized HTML result. This prevents injected page scripts from intercepting the rendering logic or the marked/hljs libraries.

### MDH wrapper persistence

The original Markdown source is saved in the `title` attribute of a hidden `<div>` inserted as a child of the rendered element, prefixed with `MDH:`. This div has no visible size. The choice of `<div title>` was made because it survives round-trips through most webmail client DOM manipulations (documented in `markdown-here.js` comments and GitHub issue [#85](https://github.com/adam-p/markdown-here/issues/85)).

### Optional host permissions

Broad host permissions (`http://*/*`, `https://*/*`) are **optional** — never granted at install. The `activeTab` permission is sufficient for the primary use case (user-triggered render). Optional permissions are only requested for the forgot-to-render automatic check, which must observe send button clicks on known domains.

### External requests

| Feature | External endpoint | Condition |
|---------|------------------|-----------|
| Math rendering | `https://latex.codecogs.com/png.image?...` | Only when `math-enabled` is true and content contains `$...$` or `$$...$$` |
| All other features | None | No external requests |

> [!WARNING]
> Math rendering sends LaTeX expression text to an external service (codecogs.com). Users who work with sensitive content in emails should be aware of this and can leave math rendering disabled (the default).

---

## 7. Developer Internals

### Repository layout

```
markdown-here/
├── src/
│   ├── manifest.json              Extension manifest (v3, shared Chrome/Firefox)
│   ├── chrome/
│   │   ├── backgroundscript.js    Service worker / background page
│   │   ├── background.html        Background page loader (Firefox / legacy)
│   │   ├── contentscript.js       Per-tab content script
│   │   ├── upgrade-notification-popup.html
│   │   └── upgrade-notification-popup.js
│   ├── common/
│   │   ├── markdown-here.js       Core render/revert toggle logic
│   │   ├── markdown-render.js     marked.js wrapper + math + hljs integration
│   │   ├── marked.js              Bundled Markdown parser (GFM fork)
│   │   ├── options-store.js       Storage abstraction + defaults
│   │   ├── options.js / .html     Options page
│   │   ├── options-iframe.js / .html  Live preview iframe
│   │   ├── common-logic.js        Forgot-to-render detection/prompt
│   │   ├── content-permissions.js chrome.permissions wrapper
│   │   ├── utils.js               Shared utilities
│   │   ├── default.css            Default render stylesheet
│   │   ├── jsHtmlToText.js        Google HTML-to-text utility
│   │   ├── mdh-html-to-text.js    MDH-specific HTML-to-text (pre-render extraction)
│   │   ├── forgot-to-render-prompt.html  Prompt UI template
│   │   ├── vendor/
│   │   │   └── dompurify.min.js   DOMPurify HTML sanitizer
│   │   └── highlightjs/
│   │       ├── highlight.js       Syntax highlighting engine
│   │       └── styles/            100+ CSS theme files
│   └── _locales/                  i18n messages (de, en, es, fr, it, ja, ko, pl, pt_BR, ru, tr, zh_CN, zh_TW)
├── utils/
│   ├── build.js                   Build script (produces dist/chrome.zip, dist/firefox.zip)
│   └── package.json               Build dependencies
├── test/                          Top-level test harness entry
├── src/common/test/               Unit tests (mocha + chai, run in browser)
├── Makefile
├── CHANGES.md
├── CONTRIBUTING.md
└── PRIVACY.md
```

### Rendering pipeline internals

**Pre-render extraction (`mdh-html-to-text.js` + `jsHtmlToText.js`):**

The content script extracts plain text from the HTML currently in the `contenteditable` field before sending it to the background for rendering. This is necessary because web clients store field content as HTML (with formatting from previous pastes, etc.), not plain text. The extractor strips HTML tags while preserving newlines and whitespace meaningful to Markdown.

**marked.js customizations (`markdown-render.js`):**

The bundled marked.js fork has several MDH-specific modifications:

- Heading IDs — sanitized with `sanitizeLinkForAnchor()` when `header-anchors-enabled` is true.
- Link renderer override — prepends `https://` to schemeless URLs.
- Math support — `userprefs['math-enabled']` injects a `mathify()` function as `markedOptions.math`, which substitutes `$...$` and `$$...$$` with `<img>` tags pointing to codecogs.com.
- highlight.js integration — `markedOptions.highlight` callback calls `hljs.highlight()` with the detected language, prefixing the class with `hljs language-` for CSS targeting.
- Always-on options: `gfm: true`, `tables: true`, `smartLists: true`, `smartypants: true`, `pedantic: false`.

**Post-render style inlining (`markdown-here.js → makeStylesExplicit`):**

After marked.js produces HTML, `makeStylesExplicit()` inlines all CSS rules from `main-css` and `syntax-css` as `style=""` attributes on every matching element. This is required because:
- Most webmail clients strip `<style>` tags and `<link>` elements when the email is sent.
- Inline styles survive the stripping and arrive at the recipient.

### Forgotten-to-render detection heuristics

`CommonLogic.probablyWritingMarkdown()` runs on a ≤10KB excerpt and returns `true` if any of these patterns match:

| Pattern | Detection |
|---------|-----------|
| Bullet list | ≥2 lines starting with `* `, `+ `, or `- ` |
| Backticks | Any backtick character |
| Math | `$...$` pattern (not just dollar signs at start/end of words) |
| Bold/strong emphasis | `**...**` or `__...__` |
| Headers | `## ` through `###### `, or underlines of ≥5 `=` or `-` |
| Links | `](` or `][` patterns |

Single-asterisk emphasis is intentionally excluded to reduce false positives in ordinary text.

### Code patterns

- **Module pattern**: All common scripts use IIFEs exporting via `module.exports` (Node.js) or `this.Symbol = Symbol` (browser global). This allows the same files to be used in tests (CommonJS require) and in the extension (global namespace).
- **Messaging**: Content scripts never call the rendering logic directly; they send messages via `Utils.makeRequestToPrivilegedScript()` which wraps `chrome.runtime.sendMessage()` with a promise-style callback.
- **DOM safety**: All HTML insertion goes through `Utils.saferSetOuterHTML()` or `Utils.safelyParseHTML()` (DOMPurify wrappers). Direct `innerHTML` assignment is not used in user-facing paths.
- **Style attribute merging**: `makeStylesExplicit()` appends inlined styles to any existing `style=""` value and ensures trailing semicolons are present before appending, preventing malformed style strings.

### Testing

Tests are browser-based (run in a test HTML page) using **mocha** and **chai**, located in `src/common/test/`. Test files:

| File | Coverage |
|------|----------|
| `markdown-here-test.js` | Core render/revert toggle logic |
| `markdown-render-test.js` | Marked.js rendering output |
| `mdh-html-to-text-test.js` | HTML-to-text extraction |
| `options-store-test.js` | Storage abstraction |
| `common-logic-test.js` | Forgot-to-render heuristics |
| `utils-test.js` | Shared utilities |

To run tests: open `src/common/test/index.html` (or `test/index.html`) in a browser with the extension installed, or use the test runner script.

### Contributing

See `CONTRIBUTING.md` in the repository root. The project uses a CLA (Contributor License Agreement); both individual and entity CLAs are available in `CLA-individual.md` and `CLA-entity.md`. Contributors are listed in `contributors/`. The project follows the `standard` JavaScript style (enforced via the ESLint config in `.eslintrc.json`).

> [!NOTE]
> The `.eslintrc.json` enforces strict single-quote strings, 2-space indent, `stroustrup` brace style, and disallows `eval`, prototype extension, and implied eval. The project targets ES6+ syntax with CommonJS module support.

---

## 8. Integration Points

### Supported email clients and web apps

The extension works on any `contenteditable` rich-text field. The following clients are specifically tested or referenced in the source:

| Client | Notes |
|--------|-------|
| Gmail | Primary target. Forgot-to-render supported. Signature detection via `[role="button"][tabindex="1"][aria-label][data-tooltip]` selector. |
| Fastmail | Forgot-to-render supported via `[class~="s-send"]` selector. |
| Other webmail | Should work if they use `contenteditable` rich-text compose fields. |
| Google Docs | Not supported — does not use standard `contenteditable`. |
| Thunderbird | Historically supported via XPI; current build script references Thunderbird but notes it as TODO. |

### i18n

The extension is localized for 13 languages via the standard WebExtension `_locales/` mechanism. All user-visible strings in JavaScript are retrieved via `Utils.getMessage()` which wraps `chrome.i18n.getMessage()`. The manifest uses `__MSG_app_name__` and `__MSG_app_slogan__` message keys.

Supported locales: `de`, `en`, `es`, `fr`, `it`, `ja`, `ko`, `pl`, `pt_BR`, `ru`, `tr`, `zh_CN`, `zh_TW`.

---

_Generated May 2026 from adam-p/markdown-here at e00d005. AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it. Unofficial; not affiliated with or reviewed by the project._
