# Caret

*A caret is the ^ a proofreader draws to slot a correction into a line — and
the cursor you are typing with right now.*

[caretgrammar.com](https://caretgrammar.com)

Underlines spelling and grammar mistakes as you type, in any text box on any
site, and lets you fix them from a hover card. Like Grammarly, except it is
yours: nothing you type leaves the computer unless you switch on the optional
Claude pass yourself, there is no account, and there is nothing to sign in to.

![The hover card over a draft e-mail](docs/screenshot-card.png)

Click the little count in the corner of the field for everything at once:

![The list of suggestions with a fix-all button](docs/screenshot-list.png)

---

## Install it in Edge

The extension is not in any store — it is loaded straight from this folder.

1. Unzip `caret.zip` somewhere you will not move or delete —
   `C:\Users\you\Documents\extensions\` or similar, **not** Downloads (see
   *Keeping it* below). You should end up with a folder called `caret` with
   `manifest.json` directly inside it.
2. Open Edge and go to **`edge://extensions`**.
3. Turn on **Developer mode** (bottom-left toggle).
4. Click **Load unpacked** and pick the `caret` folder — the folder itself,
   not a file inside it.
5. It appears in the list as **Caret**. Click the puzzle-piece icon in
   the toolbar and pin it so the on/off switch is one click away.
6. Reload any tab you already had open. Content scripts only attach to pages
   loaded after the extension was installed.

The settings page opens by itself the first time. You can reach it again from
the toolbar button → **Settings**, or by right-clicking any page.

It works the same way in Chrome (`chrome://extensions`), and in Brave, Vivaldi
and Opera.

### Keeping it

Loaded-unpacked extensions stay installed across restarts, but Edge disables
them if the folder moves or is deleted — so put the folder somewhere permanent
before you load it, not in Downloads. After editing any file, press the
**reload** ⟳ button on its card in `edge://extensions`, then reload your tab.

Edge may show a "Disable developer mode extensions" nag now and then. Click
away from it; the extension keeps working. (Choosing "Disable" turns it off,
and you re-enable it on the extensions page.)

---

## Using it

- **Underlines appear** in the field you are typing in, about half a second
  after you stop typing. Straight lines by default; there is a wavy option in
  the settings if you prefer the classic squiggle.
  - <b>Red</b> — spelling.
  - <b>Blue</b> — grammar: confused words, verb forms, agreement, articles,
    and capitals.
  - <b>Purple</b> — punctuation: apostrophes, spacing, repeated marks.
  - <b>Teal</b> — clarity: wordy or redundant phrasing.
- **Hover an underline** for the explanation and the fix. Click a suggestion
  and it is written into the field — your cursor stays where it was, and
  <kbd>Ctrl</kbd>+<kbd>Z</kbd> undoes it like anything you typed.
- **Click an underline** to pin the card open. <kbd>Esc</kbd> closes it.
- **The count badge** in the bottom-right corner of the field lists everything
  found, with a **Fix all** button for the unambiguous ones.
- **Add to dictionary** on a spelling card teaches it a word for good.
  **Ignore** silences that one complaint about that one word everywhere.
- **<kbd>Alt</kbd>+<kbd>G</kbd>** opens the card for the word at the cursor —
  handy without a mouse. **<kbd>Alt</kbd>+<kbd>Shift</kbd>+<kbd>G</kbd>**
  re-checks the field immediately, including the Claude pass if it is on.
  Change these at `edge://extensions/shortcuts`.
- **Right-click** a text box for *Check this field now* and *Turn off on this
  site*.

### Capitals

Capitalization gets its own attention, because it is what people notice:

| You type | It offers |
|---|---|
| `hello, my name is timm` | `Hello` … `Tim` — misspelled names come back capitalized |
| `i saw sarah on monday` | `I`, `Sarah`, `Monday` |
| `we flew to england in january` | `England`, `January` |
| `DOes this work` | `Does` — the shift key held a moment too long |
| `THis is a TEst` | `This`, `Test` |

Names that are also ordinary words are left alone — *a rose by any other
name*, *I will mark the calendar in may* — unless the sentence is plainly
introducing one, as in *hi, my name is rose*. Genuine acronyms and their
plurals (`PDF`, `CDs`, `TVs`, `PTO`) are never touched.

### Where it cannot work

Not the extension's choice — the browser forbids it:

- `edge://` pages, the Edge Add-ons store, and the browser's own PDF viewer.
- `file://` pages, unless you tick **Allow access to file URLs** on the
  extension's card in `edge://extensions`.
- Editors that draw their own text on a canvas instead of using a real text
  box. Google Docs is the notable one — there is no text in the page for any
  extension to read.

Some sites ask extensions not to touch a particular box (`data-gramm="false"`,
the same flag Grammarly honors, plus `data-caret="false"`). Those are
left alone, as are password fields
and boxes marked as card numbers or one-time codes.

---

## Settings

Toolbar button → **Settings**.

| | |
|---|---|
| **What to check** | Turn whole categories off. *Extra picky* (passive voice, filler words, missing commas) is off by default because it is noisy. |
| **Flag names and unusual words** | Off means a capitalized word it does not recognize is left alone, so surnames stop getting underlined. On is stricter. |
| **Count badge** | Hide the little number if you find it fussy. |
| **Underline** | Straight line (default) or wavy. |
| **My dictionary** | Everything you have added, and a box to add more. |
| **Turned off on** | Sites you have switched it off for. |
| **Ignored suggestions** | One button to forget them all. |

Everything lives in this browser profile's extension storage. Nothing syncs.

---

## Optional: proofreading with Claude

The built-in checks are instant, offline, free, and good at mechanical
mistakes. What they cannot do is judge a sentence. If you want that, put an
[Anthropic API key](https://console.anthropic.com) in the settings page and
tick **Also proofread with Claude**.

**It never rewrites you.** A Claude suggestion may swap a word, or at most a
short phrase — anything longer is thrown away before it reaches the screen,
and nothing at all changes until you click the suggestion yourself. Claude's
suggestions are also left out of *Fix all*, so each one needs its own click.

**Keeping the bill small.** Out of the box:

| | |
|---|---|
| **When it runs** | Only when you ask — `Alt+Shift+G`, or right-click → *Check this field now*. Switch it to automatic in settings if you want it as you type. |
| **Model** | Haiku 4.5, the cheapest. Sonnet 5 and Opus 5 are in the dropdown if you want a sharper reader. |
| **Effort** | Low. (Haiku has no effort setting — it is always the quick one.) |
| **Hard cap** | 40 checks a day, then it stops until tomorrow. Change the number in settings; the page shows how many are left. |
| **Repeats** | Identical text is never sent twice, and requests are throttled to one per 1.2 seconds. |
| **Length** | At most 6,000 characters of the field. |

At Haiku's rates, one check of a long paragraph is well under a cent, so the
default cap is a few cents a day at the very most. For a hard ceiling that the
extension cannot exceed, set a spend limit on the key itself in the Anthropic
Console under **Billing → Limits**.

**It has to be an API key.** There is no way to make this — or any third-party
app — bill a Claude Pro or Max subscription instead. Subscriptions cover
Claude.ai and Claude Code; the API bills prepaid credits per request, and an
API key is how you get at it. The OAuth tokens the `claude` CLI uses are
issued to Anthropic's own client and still bill the same API credits, so
they buy you nothing here.

Never sent, whatever the setting says: password fields, anything that looks
like a card number or a national ID, and anything that looks like a key or a
token. **Do read the caveat:** the key sits in extension storage unencrypted,
which is normal for a personal extension but means anyone with your unlocked
profile can read it. Use a key you can rotate, and press **Test the key** after
pasting it.

---

## What is in here

```
manifest.json          the extension itself (Manifest V3)
src/engine/            the checker: no browser APIs, plain ES modules
  tokenize.js          text -> words, sentences, and the bits to leave alone
  dictionary.js        the word list, frequency ranking, known-typo map
  spelling.js          is this word wrong, and what did you mean
  rules.js             ~90 grammar, punctuation and clarity rules
  engine.js            runs everything, resolves overlapping findings
  ai.js                the optional Anthropic API call
src/background/        service worker: owns the dictionary and the API key
src/content/           what runs in the page
  adapters.js          reading, measuring and editing input/textarea/contenteditable
  overlay.js           the underlines, the card and the badge (shadow DOM)
  controller.js        when to check, what to draw, what a click means
src/options/, src/popup/   the settings page and the toolbar panel
data/                  generated word lists (1.4 MB, gzipped), including the
                       proper nouns that drive the capitalization checks
tools/build-data.py    rebuilds data/ from public word lists
tools/make-icons.py    draws the toolbar icons
test/                  80 unit tests, 18 browser tests
```

**How the underlines are drawn.** A `<textarea>` has no DOM inside it, so there
is nothing to ask for the position of a word. The extension keeps an off-screen
mirror `<div>` wearing the field's exact typography, puts the same text in it,
measures the word there with a `Range`, then shifts those rectangles onto the
real field and clips them to its visible box. A `contenteditable` is measured
directly. Either way the page's own DOM is never modified — that is what breaks
rich editors — and the underlines live in a shadow root of their own so no site
stylesheet can reach them.

**Why the dictionary lives in the service worker.** It is 371,000 words. Loading
that into every frame of every tab would be absurd, so the page sends text to
the worker and gets back a list of `{start, end, suggestions}`.

---

## Working on it

```
npm test          # 105 unit tests: rules, offsets, precision, the Claude request
npm run test:e2e  # 29 browser tests: loads the extension into Chromium
npm run lint      # eslint
npm run data      # rebuild data/ (downloads the source word lists)
npm run icons     # redraw the icons
npm run zip       # a zip of the folder, for copying to another machine
```

`npm run test:e2e` needs Playwright (`npm install --save-dev playwright`); it
drives a real Chromium with the extension loaded. The first file checks the
everyday path — underlines land inside the field, the card opens on hover,
clicking a suggestion edits the text, password fields are skipped. The second
(`e2e-hard.mjs`) is the awkward stuff that broke during development: a padded
and scrolled textarea, a right-aligned input, a field inside
`transform: scale()`, right-to-left text, a contenteditable with `<b>` and
`<i>` in it, an iframe, a React-style controlled input that echoes its value
back, and a 10,000-character document.

The test that matters most is the *clean prose* set in
`test/engine.test.mjs`: two dozen correct sentences that must produce **zero**
underlines. A checker that cries wolf gets switched off, so when you add a rule,
add a sentence it must not fire on.

### Adding your own rule

`src/engine/rules.js`, in the list at the top:

```js
{
  id: 'school-nickname',
  category: 'style',
  short: 'House style',
  re: /\bECS\b/g,
  message: 'Write the school name out in full.',
  suggest: () => ['Englewood Christian School'],
}
```

Reload the extension, and it applies everywhere. `group: 2` underlines only the
second capture group; `exclude: (m) => …` drops a match after a second look.

### The word lists

Built by `tools/build-data.py` from three public sources — a 370k English word
list (dwyl/english-words, Unlicense), a frequency list from OpenSubtitles
(hermitdave/FrequencyWords, MIT) used to rank suggestions, and Wikipedia's
machine-readable list of common misspellings (CC BY-SA 4.0). The generated
files in `data/` are committed, so a normal install never runs the script.

---

## Troubleshooting

**No underlines anywhere.** Reload the tab — content scripts only attach to
pages opened after the extension was loaded. Then check the toolbar switch and
that the site is not in the turned-off list.

**Nothing in one particular box.** It may be a canvas editor (Google Docs), a
password field, or a box the site flagged with `data-gramm="false"`.

**Underlines in the wrong place.** Almost always a field whose font loads late;
scrolling or typing re-measures it. If it persists on a specific site, that
site is doing something unusual with transforms or zoom — worth a note in
`adapters.js`.

**The first check after a while is slow.** The service worker had gone to sleep
and rebuilt the dictionary — about half a second, once.

**Claude proofreading does nothing.** Settings → **Test the key**. The error it
prints (bad key, no credit, rate limit) is the actual API response. Remember it
only runs when you ask — `Alt+Shift+G` — unless you switched it to automatic.
