Skip to main content

Architecture

Layers at a glance

LayerFilesRole
Content scriptscontent/, shared/ (chunker, i18n)Extract, chunk, play, highlight — runs in the host page
Panel UIcontent/floating-panel.js + .cssAll controls, isolated in a Shadow DOM so host CSS cannot restyle them
Style injectionshared/style-inject.jsApplies the widget's CSS without requiring style-src 'unsafe-inline'
Editor adapterscontent/adapters/Google Docs and Word for the web — see Hosted Editors
Word lookupcontent/lookup-popover.jsTeanglann dictionary popover — see Word Lookup
TTS fetchbackground/ (extension) or embed/src/shim.js (widget)Talks to the TTS API; prefetches upcoming chunks
Embed serverembed/Serves the widget bundle at webreader.abair.ie
i18nlocales/resources.json + shared/i18n.jsTranslation strings — source of truth in resources.json, managed via the Translations Editor
Manifestsmanifest.json / manifest-firefox.json / -safari.jsonChrome MV3, Firefox MV2, Safari MV2 (extension only)

The extension and embed widget share the entire content-script layer. Only the TTS fetch path and the host glue differ.

Reading pipeline

When Read this page fires:

  1. Pick a container — first match wins: article → main → [role="main"] → .post-content → … → body. Anything outside is silent.
  2. Collect candidates — one querySelectorAll('p, h1–h6, li, td, th, blockquote, button, a, figcaption'), plus img[alt] if Announce media is on.
  3. Filter — drop hidden, in-panel, in-Shadow-DOM, and editable elements. Headings need at least 1 character; everything else at least 2.
  4. Cap — extraction stops at MAX_PAGE_CHARS (50,000). Longer pages are truncated and the panel says so.
  5. Chunk each candidate by sentence (default) or paragraph; the chunker handles abbreviations and inline <a> annotations.
  6. Pause between chunks — headings 600 ms, block prose 250 ms, other 0.
  7. Prefetch + play — TTS the next two while the current plays; the highlighter syncs words back to the DOM via XPath.

How highlighting touches the page

The highlighter wraps the page's own text in <span> elements in order to colour it. Two rules keep that safe on a page the widget does not own:

  • Editable regions are read but never highlighted. Anything inside [contenteditable] is skipped by the DOM mapping. Rich-text editors such as ProseMirror, TipTap, Quill and CKEditor watch their own DOM, and would otherwise fold the injected spans into the saved document.
  • Unwrapping restores the original text nodes. Wrapping uses Text.splitText(), and clearing puts the pieces back into the node they came from. Text-node boundaries that existed before the widget ran are never merged, which is what lets frameworks such as React keep their references to those nodes.

Things integrators must know

  • Reading order = DOM order. CSS reordering (flex order, grid placement, absolute positioning) is invisible.
  • Nested <a> is deduplicated. Card patterns (<a> wrapping <h2> + <p>) collapse to one chunk. Inline links in prose inject "Link: " into the parent's audio instead of duplicating the text.
  • Still duplicates: <button> in <p>/<li>, nested <li>, and bare-link containers (<h2><a>X</a></h2>).
  • Form fields are silent. <input>, <select>, <textarea>, <label> aren't in the selector. Describe forms in a sibling <p> if listeners need them.
  • <div> and <span> are invisible to the chunker. Wrap meaningful text in one of the tags above, or add a visually hidden <p>.
  • aria-hidden="true" is ignored. Only display / visibility filter elements.
  • Very short text is dropped. Single decorative characters (bullets, arrows) are skipped, while short Irish headings such as "Pobal" are kept.

Drilling in

Want to understand…Read
Smart link handlingshared/chunker.jsbuildInlineAnnotatedText
Pipeline orchestrationcontent/content.jsgetTextNodes, playCurrentChunk
Word-level highlight synccontent/highlighter.jsstartWordSync
Non-destructive wrap / unwrapcontent/highlighter.jswrapTextInNode, clear
CSP-safe stylesheet injectionshared/style-inject.jsadopt
Extension boot vs widget bootbackground/service-worker.js vs embed/src/boot.js
Message API between content and BGsynthesize, prefetch, cancelPrefetch, audioData, audioError

Last updated 2026-07-22