Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/zotero/zotero-connectors/llms.txt

Use this file to discover all available pages before exploring further.

Every HTTP and HTTPS page a user visits receives a silently injected copy of the Zotero translation framework. These content scripts run in an isolated JavaScript context inside the tab, giving them full read access to the live DOM while remaining sandboxed from the page’s own scripts. They are responsible for detecting which translators apply to the current page, running detectWeb() and doWeb() against the DOM when the user clicks the toolbar button, and communicating results back to the background process for saving.

How Content Scripts Are Loaded

The manifest’s content_scripts block is populated at build time by gulpfile.js. For MV3 the entry looks like:
{
  "matches": ["http://*/*", "https://*/*"],
  "run_at": "document_start",
  "js": [/*INJECT SCRIPTS*/]
}
The /*INJECT SCRIPTS*/ placeholder is replaced by the injectIncludeManifestV3 array (derived from injectInclude in gulpfile.js), so every listed file is executed in order at document_start — before the page’s own scripts run.
For MV2 (Firefox) the array is injectIncludeBrowserExt, which omits inject/virtualOffscreenTranslate.js (not needed without a service-worker constraint) but is otherwise identical.

The injectInclude Bundle

The following scripts are concatenated into the inject bundle in this order (from gulpfile.js):
var injectInclude = [
  'zotero_config.js',
  'zotero.js',
  'translate/promise.js',
  'utilities/date.js',
  'utilities/openurl.js',
  'utilities/xregexp-all.js',
  'utilities/xregexp-unicode-zotero.js',
  'utilities/resource/zoteroTypeSchemaData.js',
  'utilities/utilities.js',
  'utilities/utilities_item.js',
  'utilities.js',
  'http.js',
  'proxy.js',
  'translate/debug.js',
  'utilities/schema.js',
  'translate/rdf/init.js',
  // … RDF parser files …
  'translate/translation/translate.js',
  'translate/translation/translate_item.js',
  'translate/translator.js',
  'translate/utilities_translate.js',
  'inject/http.js',
  'inject/sandboxManager.js',
  'translateWeb.js',
  'itemSaver.js',
  'inject/pageSaving.js',
  'integration/connectorIntegration.js',
  'cachedTypes.js',
  'schema.js',
  'messages.js',
  'messaging_inject.js',          // monkey-patches all MESSAGES entries
  'inject/progressWindow_inject.js',
  'inject/modalPrompt_inject.js',
  'messagingGeneric.js',
  'i18n.js',
  'singlefile.js',
  // … then inject/inject.js last …
];
messaging_inject.js and messagingGeneric.js are loaded before inject.js so that the Zotero.Messaging namespace is fully set up before Zotero.Inject.init() runs.

The shouldInject Guard

Before doing any real work the script checks several conditions to decide whether this frame should participate in translation at all (src/common/inject/inject.jsx):
var isTopWindow = false;
if (window.top) {
  try {
    isTopWindow = window.top == window;
  } catch(e) {}
}

// Hidden iframes (display:none) used for scraping should not self-inject
var isHiddenIFrame = false;
try {
  isHiddenIFrame = !isTopWindow && window.frameElement
    && window.frameElement.style.display === "none";
} catch(e) {}

// Only text/html and application/pdf iframes are useful
const isAllowedIframeContentType =
  ['text/html', 'application/pdf'].includes(document.contentType);

// Only run on real web pages (not file://, not extension pages)
const isWeb =
  window.location.protocol === "http:" ||
  window.location.protocol === "https:";

const isTestPage = Zotero.isBrowserExt
  && window.location.href.startsWith(browser.runtime.getURL('test'));

// Final gate: web (or test), not a hidden iframe, top window or allowed iframe type
const shouldInject =
  (isWeb || isTestPage) && !isHiddenIFrame
  && (isTopWindow || isAllowedIframeContentType);
If shouldInject is false, Zotero.Inject.init() returns immediately.

Zotero.Inject.init()

The init() method is the entry point for the inject layer. It waits for the page to become visible (handling pre-render states), then:
Zotero.Inject = {
  async init() {
    if (!shouldInject) return;

    await Zotero.initInject();
    // Zotero namespace APIs now initialized

    document.addEventListener("ZoteroItemUpdated", function() {
      Zotero.debug("Inject: ZoteroItemUpdated event received");
      Zotero.Messaging.sendMessage("pageModified", null);
    }, false);

    this._addMessageListeners();
    this._addZoteroButtonElementListener();
    this._handleOAuthComplete();

    if (document.readyState !== "complete") {
      window.addEventListener("pageshow", function(e) {
        if (e.target !== document) return;
        return Zotero.PageSaving.onPageLoad(e.persisted);
      }, false);
    } else {
      return Zotero.PageSaving.onPageLoad();
    }
  },
  // …
};

// Defer init until page is visible if prerendering
if (document.visibilityState == 'prerender') {
  var handler = function() {
    Zotero.Inject.init();
    document.removeEventListener("visibilitychange", handler);
  };
  document.addEventListener("visibilitychange", handler);
} else {
  Zotero.Inject.init();
}
The ZoteroItemUpdated custom DOM event is fired by third-party page scripts (e.g. publisher sites that use the Zotero save button element). When the inject layer catches it, it notifies the background via Zotero.Messaging.sendMessage("pageModified", null) so the toolbar icon can be updated.

Message Listeners Registered in _addMessageListeners()

Message nameWhat it does in inject
translateChecks instanceID, then calls Zotero.PageSaving.onTranslate(...data) to run the selected translator
saveAsWebpageCalls Zotero.PageSaving.onSaveAsWebpage(data) to capture the page as a snapshot
updateSessionDelegates to Zotero.PageSaving.onUpdateSession(data)
pageModifiedDebounced (1 000 ms) call to Zotero.PageSaving.onPageLoad(true) to re-run detection
historyChangedSame debounced re-detection — catches SPA navigation
firstUseShows the first-run welcome prompt via Zotero.Inject.firstUsePrompt()
expiredBetaBuildShows the build-expired prompt
clipboardWriteCalls navigator.clipboard.writeText(text) — clipboard API is unavailable in the background
Three additional listeners — ping, confirm, and notify — are registered separately at the top of inject.jsx, before the Zotero.Inject object is defined, and only when isTopWindow is true:
Message nameWhat it does in inject
pingReturns 'pong' so the background can verify scripts are injected (top window only)
confirmShows a modal confirm dialog via Zotero.Inject.confirm(props) (top window only)
notifyInjects and shows a Zotero.UI.Notification bar into the DOM (top window only)

Translator Detection Flow

1

Page load

Zotero.PageSaving.onPageLoad() is called. It delegates to Zotero.Translate.Web which calls Zotero.Translators.getWebTranslatorsForLocation(url, rootURL).
2

URL-match in background

getWebTranslatorsForLocation is a monkey-patched method (registered in MESSAGES). The call is transparently forwarded to the background process, which performs the fast regex-based URL match against the cached translator list and returns matching translators.
3

detectWeb() in page

For each returned translator, Zotero.Translate.Web runs detectWeb(doc, url) inside the SandboxManager — an eval-based sandbox that gives translator code access to the live DOM.
4

Report back to background

Zotero.Connector_Browser.onTranslators(translators, instanceID, contentType) is called. Because Connector_Browser is also monkey-patched, this message is forwarded to the background, which updates _tabInfo[tab.id].translators and refreshes the toolbar icon via _updateExtensionUI(tab).

pageSaving.js and sandboxManager.js

pageSaving.js

Implements Zotero.PageSaving — the high-level orchestration of page-load detection, translation, and save-as-webpage flows. It manages the progress window and talks to Zotero.ItemSaver to dispatch translated items to the background.

sandboxManager.js

Implements Zotero.Translate.SandboxManager. Because content scripts cannot use eval() with external code in a straightforward way, the sandbox uses a carefully scoped eval wrapper that prepends sandbox properties into the evaluated code. This lets translator JavaScript access Zotero, doc, and url as if they were global.

Google Docs Special Content Scripts

Google Docs pages receive an additional set of content scripts declared separately in the manifest:
{
  "matches": ["https://docs.google.com/document/*"],
  "run_at": "document_start",
  "js": ["zotero-google-docs-integration/kixAddZoteroMenu.js"]
},
{
  "matches": ["https://docs.google.com/document/*"],
  "run_at": "document_end",
  "js": [
    "zotero-google-docs-integration/googleDocs.js",
    "zotero-google-docs-integration/client.js",
    "zotero-google-docs-integration/clientAppsScript.js",
    "zotero-google-docs-integration/document.js"
  ]
}
  • kixAddZoteroMenu.js — runs at document_start, hooks into the Kix editor (Google Docs internal name) to add the Zotero menu item to the Docs Add-ons menu.
  • googleDocs.js + client.js — run at document_end, implement the Zotero ↔ Google Docs integration that allows inserting citations and bibliographies directly into a document via the Zotero desktop application.
The Google Docs integration scripts communicate with the Zotero desktop app through a separate protocol layer. They do not use the standard MESSAGES registry because they need to support long-lived bidirectional sessions.

Build docs developers (and LLMs) love