Skip to content

Repository files navigation

Buoy

A floating outline and section-aware search for long web articles. Runs entirely on your machine — the extension has no network access at all.

Ctrl-F tells you "1 of 47". Buoy tells you where those 47 are.

Buoy in action: searching a long article and seeing the matches grouped by section


What it does

Open a long article. A small bubble appears in the corner. Click it and you get:

1. A structure outline. Buoy splits the article into sections. It uses the page's own h1h6 headings as hard boundaries — the author's headings are always better than anything an algorithm would invent. Only a heading-less run longer than ~800 words gets split automatically, using lexical cohesion (TextTiling), and those sections are labelled auto-split so you know the machine made them.

2. Keywords per section. Not fake AI-written titles — just the terms that are distinctive to each section. Scored with TF-IDF where each section is treated as a document and the page as the corpus, which is what makes "distinctive to this section" mean anything in a single article.

3. Search grouped by section. The reason this exists:

Search "cost" → 23 matches

  ▸ 3. Hidden costs in the supply chain   12  ████████
  ▸ 7. Pricing strategy                    6  ████
  ▸ 1. Intro                               3  ██
  ▸ [auto-split] margin · inflation        2  █

You see at a glance where a term is concentrated. Expand any group for context snippets; click to jump and highlight.

Privacy

Buoy has no network code. Not "we promise not to send your data" — there is no code capable of sending anything.

  • manifest.json requests exactly one permission: storage. No host_permissions.
  • CI fails the build if fetch, XMLHttpRequest, WebSocket, sendBeacon or friends appear anywhere in the source. See scripts/check-no-network.sh.
  • No AI API, no account, no analytics, no telemetry.

The complete list of what is stored, ever:

{
  minReadingMinutes: 5,          // when the bubble appears
  minHeadingsOverride: 4,
  panelSide: 'right',
  bubblePosition: { x, y },
  theme: 'auto',
  uiLanguage: 'auto',
  pinnedTerms: [],
  disabledDomains: [],
}

Page content, page titles, URLs and search queries are never written anywhere.

Install

Not on the Chrome Web Store yet.

git clone https://github.com/YOUR_NAME/buoy.git

Then: chrome://extensions → enable Developer modeLoad unpacked → select the buoy/ folder.

There is no build step and no dependencies. What you clone is what runs.

What it does not support

Be clear about this before you install:

Not supported Why
Scanned PDFs The pages are images. Reading them needs OCR, which needs a model — out of scope for an offline, zero-dependency tool. Buoy tells you this rather than showing an empty outline.
Auto-opening PDFs you click on the web Intercepting a PDF URL means fetching its bytes, which means shipping network code and host permissions. That would end the guarantee below, so it is opt-out by design: drag the file in instead.
Google Docs Renders to <canvas> since 2021; there is no readable text in the DOM. The only route is the Docs API + OAuth, which would break the offline guarantee.
Notion and similar Virtualised scrolling — only the visible screen exists in the DOM.
Semantic search / AI summaries Would require a model or an API. Deliberately out of scope.
Firefox / Safari Chromium only for now. The code avoids Chrome-only APIs where it can, but this is untested elsewhere.

If you want an AI reading assistant, this is not it. Buoy is a structure-and-search tool.

PDFs (papers, reports, lecture notes)

Dragging a PDF into Buoy's reader view and searching it

Click the Buoy toolbar icon on any page that has nothing to read — or open the reader view directly — and drag a PDF in.

Buoy parses the PDF itself and rebuilds it as clean, readable HTML: paragraphs merged back from positioned glyphs, headings recovered from typography, running heads and page numbers dropped. Then the ordinary outline and section-grouped search run on top of it. There is no separate PDF codepath in the search engine — it is the same one used for web pages.

It is a reader view, not a PDF renderer: you get the text, structure and search; you do not get the original layout, figures or tables.

The parser is written from scratch (src/pdf/) — no pdf.js, no dependencies. It handles compressed object streams, subsetted CJK fonts via /ToUnicode, CID fonts, Form XObjects, and the Kangxi-radical and / ligature substitutions that would otherwise wreck word segmentation and copy-paste.

Crucially: the file never leaves the tab. It is read from a File object in memory. No fetch, no upload, no temp files — which is why the offline guarantee above is still literally true with PDF support shipped.

node tests/pdf-extract.test.mjs   # 17 assertions against a known fixture

When the bubble appears

Automatically when either:

  • estimated reading time ≥ 5 minutes, or
  • the article has ≥ 4 real headings

Reading time, not word count — 1200 Chinese characters and 1200 English words differ by about 1.5× in actual reading time, so a raw character threshold means different things in different languages. Rates used: 350 chars/min for Chinese, 500 for Japanese, 238 words/min for English.

Clicking the toolbar icon (or Ctrl/Cmd+Shift+F) opens the panel regardless of length.

How it works

Stage Approach
Main content Text-density scoring over <p>/<li>, ancestors credited with depth decay, link-heavy blocks discarded. Navigation, sidebars, comments and footers excluded.
Tokenising Intl.Segmenter — built into V8, gives real CJK word boundaries with no dictionary download.
Sections Native headings are hard boundaries. Heading-less runs over ~800 units get TextTiling (sliding-window lexical cohesion, cut at the valleys).
Keywords TF-IDF with sections as documents; ×1.5 for terms that appear in any heading; single CJK characters discounted.
Search Substring matching on a normalised (NFKC + lowercase) flat copy of the text, with an index map back to exact DOM offsets. Necessary for CJK, and it survives a word being split across <em>/<span> boundaries.
Highlighting CSS Custom Highlight API — paints Ranges without touching the DOM, so React/Vue pages don't undo it and the site's own observers never fire.
Long documents Every pass is time-sliced (yield every 8 ms). Supported up to ~300k units; beyond that, auto-splitting is disabled and only native headings are used.
PDFs Own parser: whole-file object scan (no xref — real files break it too often), /ObjStm expansion, content-stream interpretation into positioned glyph runs, then lines from y-proximity, paragraphs from vertical rhythm and line-ending shape, headings from font size and weight.

Known limitations

Honest list — PRs welcome on all of these:

  • Intl.Segmenter splits Chinese compound terms and proper nouns. 供應鏈管理 becomes 供應/鏈/管理, so keyword quality for technical Chinese text is noticeably worse than it could be. Fixing it properly means bundling a dictionary segmenter (jieba-wasm), which costs several MB. Not done in v1.
  • Outline quality depends on the site's HTML. A page with clean headings gets a great outline; a <div>-soup page gets whatever TextTiling can find.
  • Search results are capped at 500 per query. On a very long document a common word gets truncated, which skews the density bars.
  • The outline caps at 80 sections. Beyond ~50 the list stops being scannable anyway; virtualised scrolling and collapsing by heading level are the real fix (v1.1).
  • Only the main frame is processed — content inside iframes is ignored.
  • PDF heading detection is typographic, not semantic. It reads font size and weight, so a paper with consistent styling gives a clean outline, while a PDF that fakes headings with plain body text gives none. Multi-column layouts (two-column conference papers) are read column-by-column only when the producer wrote them in that order — some will interleave.
  • PDF tables and figures are dropped, along with their captions' original placement. Text inside figures may appear as stray fragments.

Contributing

The lowest-effort useful contributions:

  • Stopword listssrc/stopwords.js is plain data. Bad keywords usually mean a missing stopword.
  • UI translationssrc/strings.js is one object per language.
  • Extraction failures — if Buoy picks the wrong content on a site, open an issue with the URL.

Run the offline guarantee locally before opening a PR:

./scripts/check-no-network.sh

Demo

demo/demo.html is a self-contained sample article (with navigation and a sidebar, so you can check they're correctly ignored). Serve the folder and open it — the engine imports directly, no extension needed:

python3 -m http.server 4175

To re-record docs/demo.gif after a UI change, with the server running:

./tools/record-demo.sh

That needs no ffmpeg and no npm install — headless Chrome writes one PNG per frame and tools/png2gif.mjs assembles the GIF using only Node's built-in zlib. The encoder decodes its own LZW output and compares it before writing, because a code-size off-by-one produces a file that some viewers render and others silently reject.

License

MIT

About

Floating outline and section-grouped search for long articles and PDFs. Runs entirely on your machine — zero network access, enforced by CI.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages