Better Super Simple Highlighter
August 9, 2026 · View on GitHub
A Chrome extension that marks up text on web pages and restores those marks each time you come back to the page. Marks live on your machine in IndexedDB, and nothing is sent anywhere.
This is a downstream build of emersonding/super-simple-highlighter, which is itself a Manifest V3 migration of the original by Dexterous Logic. What follows describes what this build changes. The original README is preserved further down, in full.
Using it
Select any text and a small toolbar appears next to the cursor.

Holding the pen opens a colour picker, so a mark can be saved in any of your first four styles without going through settings first.

The speech-bubble button saves the selection and attaches a note to it.

Marks come back when you return to the page, with a small dot on any that carry a note.

Hovering a dot shows the note.

Styles, opacity and the target for the toolbar's AI button are configurable.

Every page you have marked up is listed, searchable and exportable to Markdown.

Backups can be exported, imported, or merged into what you already have.

What this build changes
Marks are found by index instead of by scanning everything
This is the substantive change, and the reason this build exists.
Manifest V2 answered "which marks belong to this page?" with a PouchDB map/reduce view keyed
on [match, date]. Manifest V3's content security policy forbids the Function() call
PouchDB needs to compile a view, so the migration to MV3 replaced every view query with
allDocs({include_docs: true}) followed by filtering in memory.
That reads and deserializes every document in the database to answer a question about a single page, and two of those queries sit on the path of every page load: one for the toolbar badge, one to restore the page's marks.
Measured on a database of 20,000 saved selections across 400 pages, returning the 50 that belong to one page:
| Query | Scanning | Indexed |
|---|---|---|
getMatchingSum(), runs on every page load | 788 ms | 2 ms |
getMatchingDocuments(), runs on every page load | 789 ms | 4 ms |
getSums(), the Pages tab and startup tidy-up | 808 ms | 25 ms |
So about 1.6 seconds of work per page load became about 6 ms, and it no longer grows with the number of marks you have ever made, only with the number on the page in front of you.
The replacement is a secondary index, the thing the map/reduce view used to be, held in
_local documents and maintained by replaying PouchDB's changes feed. _local documents are
invisible to allDocs(), carry no revision history, and replication skips them, so the index
never reaches your backups and never appears in an export.
_local/ssh_index_meta holds the sequence cursor, the format version and the list of known
pages. One _local/ssh_index$<match> document per page then holds a compact entry per mark,
in the form [id, date, verb, correspondingDocumentId]. Those entries carry exactly the
fields the query path sorts and filters on, so ordering, limit, verbs and
excludeDeletedDocs are all resolved against the index, and only the documents that survive
that are read.
Every query first brings the index up to date by replaying db.changes({since: seq}). The
changes feed is the authority, so the index is correct however a document arrived. A mark
saved in the browser lands in the feed, and so does one restored from a backup or written by
a replication the extension code never touched. When the stored sequence already matches the
database's, which is the usual case on a page load, that check costs a single _local read.
Entries are keyed by document id and overwritten in place, which makes replay idempotent, and
per-page documents are written before the cursor advances, so a failed write replays rather
than disappears.
Removals need care, because a removed document reaches the changes feed only as a tombstone with no page attached to it. Callers that know the page now say so, and reads verify their entries with a key-only lookup and repair the page's index when a document has gone. Between the two, a removal cannot leave a phantom mark behind.
The index is only ever a cache. It is rebuilt from one full scan if it goes missing, if the format version moves, or if the database is reset underneath it. Nothing depends on it surviving, so the worst case is one slow read.
Implementation is in src/shared/db_index.js, used from getMatchingDocuments(),
getMatchingSum() and getSums() in src/shared/db.js.
The About page logo
The manifest states icon paths relative to the extension root. options.html used to sit at
the root, so binding an image straight to manifest.icons worked, but the page moved to
src/options/ during the project reorganisation and the browser began resolving that path
against the new folder, leaving a broken image. It now resolves through
chrome.runtime.getURL().

Tests that can run unattended
The end-to-end suite launched a headed browser, so it needed a display server and could not
run in CI. Extensions load fine under Chromium's modern headless mode, so the suite now runs
headless by default, and HEADED=1 brings the window back when you want to watch it.
A GitHub Actions workflow runs both suites on every push. Nothing ran them before.
tests/e2e/db-index.spec.js adds eight tests that drive the index against a real PouchDB
inside the service worker. The core ones compare its results against a brute-force scan of
the whole database, which is the implementation it replaced, across every combination of
query options, so the index is checked against the source of truth rather than against
expectations written by hand. The rest cover building the index from documents that predate
it, incremental updates, both removal paths, repair after drift, and rebuilding once the
index is discarded or left behind by an older format.
tests/unit/db_index.test.js adds eleven tests covering entry encoding, replay idempotency
and page totals.
The suite stands at 20 unit tests and 41 end-to-end tests.
Installing
Grab the zip from Releases,
unpack it somewhere permanent, then open chrome://extensions/, turn on Developer mode,
click "Load unpacked" and pick the unpacked folder.
Leave that folder where it is. Chrome reads an unpacked extension from its path on every start, so moving or deleting it disables the extension.
Developing
npm install
npx playwright install chromium
npm test
npm test runs the unit tests and then the end-to-end suite. npm run test:perf reproduces
the measurements above, sized by the BENCH_DOCS and BENCH_PAGES environment variables.
The original README
emersonding/super-simple-highlighter, reproduced in full.
Super Simple Highlighter (Fork)
This is a fork of Super Simple Highlighter by Dexterous Logic (Copyright 2014-2017).
A Chrome extension for highlighting text on web pages, with automatic restoration of highlights on each page revisit.
The extension now uses a context-first file layout: runtime code lives under src/, and icons/images/fonts live under assets/.
Selection toolbar
When you select text on a page, the extension shows a floating toolbar positioned near the cursor location for quick actions:
- Google search: Open a new tab to search Google for the selected text
- Highlight: Save the selection with your active highlight color
- Comment & highlight: Create a highlight and attach a comment to it for later review
- AI search: Open a new tab to search your configured AI target with the selected text

Changes in this fork
- Manifest V3 migration: Upgraded from MV2 to MV3 (service worker,
chrome.scriptingAPI, etc.) - PING rejection fix: Content script injection now handles rejected PING messages correctly, allowing the inject-then-retry flow to work
- PouchDB MV3 CSP fix: Replaced
db.query()(map/reduce) withdb.allDocs()+ in-memory filtering to avoidFunction()calls blocked by MV3's strict Content Security Policy - E2E tests: Added Playwright-based end-to-end tests for highlight creation and persistence
- Selection toolbar: Added quick actions for Google search, highlight creation, highlight comments, and AI search, positioned near the cursor location with the AI action as the fourth button
Installation
Load as an unpacked extension in Chrome:
- Navigate to
chrome://extensions/ - Enable "Developer mode"
- Click "Load unpacked" and select this directory
Testing
npm install
npx playwright install chromium
npx playwright test
License
This project is licensed under the GNU General Public License v3.0 (or later), the same license as the original project. See LICENSE for the full text.
The original license and copyright notices have been preserved in full.
Third-party assets
License
GNU General Public License v3.0 or later, the same licence as the original project. See LICENSE for the full text. The original licence and copyright notices are preserved in full, and this build adds no further restrictions.
Thanks
To Dexterous Logic for writing the extension in the first place, and for the decade it spent quietly working.
To emersonding for the Manifest V3 migration, which was the hard and unglamorous part. Carrying PouchDB across the content security policy change, and working out why saved marks failed on pages whose element ids start with a digit, is work this build simply inherited.
Third-party assets
The Highlighter Blue Icon is by Hopstarter, under CC BY-NC-ND 3.0. The Exclamation Icon is by Aha-soft, under CC BY 3.0. Bundled libraries are listed on the extension's About page, each with its licence.