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.

Selection toolbar

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.

Colour picker

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

Comment field

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

Restored marks

Hovering a dot shows the note.

Note on hover

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

Styles tab

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

Pages tab

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

Backups tab

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:

QueryScanningIndexed
getMatchingSum(), runs on every page load788 ms2 ms
getMatchingDocuments(), runs on every page load789 ms4 ms
getSums(), the Pages tab and startup tidy-up808 ms25 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 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().

About tab

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

Selection toolbar screenshot

Changes in this fork

  • Manifest V3 migration: Upgraded from MV2 to MV3 (service worker, chrome.scripting API, 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) with db.allDocs() + in-memory filtering to avoid Function() 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:

  1. Navigate to chrome://extensions/
  2. Enable "Developer mode"
  3. 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.