From 7b56a3eefe84e5e61d5051ab2fa12a79712f8c47 Mon Sep 17 00:00:00 2001 From: Abe Jellinek Date: Wed, 23 Jul 2025 10:47:23 -0400 Subject: [PATCH] Cache CiteProc Engine instances, pre-cache for Quick Copy (#5399) --- chrome/content/zotero/fileInterface.js | 2 +- chrome/content/zotero/xpcom/integration.js | 11 ++-- chrome/content/zotero/xpcom/quickCopy.js | 15 ++++- chrome/content/zotero/xpcom/style.js | 66 ++++++++++++++++++---- 4 files changed, 76 insertions(+), 18 deletions(-) diff --git a/chrome/content/zotero/fileInterface.js b/chrome/content/zotero/fileInterface.js index e9fc9cd46a..40e7b9296b 100644 --- a/chrome/content/zotero/fileInterface.js +++ b/chrome/content/zotero/fileInterface.js @@ -831,7 +831,7 @@ var Zotero_File_Interface = new function() { clipboardService.setData(transferable, null, Components.interfaces.nsIClipboard.kGlobalClipboard); - Zotero.debug(`Copied bibliography to clipboard in ${new Date() - d} ms}`); + Zotero.debug(`Copied bibliography to clipboard in ${new Date() - d} ms`); } diff --git a/chrome/content/zotero/xpcom/integration.js b/chrome/content/zotero/xpcom/integration.js index 6293cbe0fb..4e6930883a 100644 --- a/chrome/content/zotero/xpcom/integration.js +++ b/chrome/content/zotero/xpcom/integration.js @@ -2231,10 +2231,13 @@ Zotero.Integration.Session.prototype.restoreProcessorState = function() { } } if (!Zotero.Prefs.get('cite.useCiteprocRs')) { - // Due to a bug in citeproc-js there are disambiguation issues after changing items in Zotero library - // and rebuilding the processor state, so we reinitialize the processor altogether - let style = Zotero.Styles.get(this.data.style.styleID); - this.style = style.getCiteProc(this.data.style.locale, this.outputFormat, this.data.prefs.automaticJournalAbbreviations); + // Due to a bug in citeproc-js there are disambiguation issues after + // modifying items in Zotero, even after calling rebuildProcessorState(), + // because rebuildProcessorState() doesn't reset three properties of the + // processor (registry, tmp, and disambiguate) used for disambiguation. + // Call the deprecated restoreProcessorState(), which resets everything. + // Revisit if restoreProcessorState() is removed. + this.style.restoreProcessorState(); } this.style.rebuildProcessorState(citations, this.outputFormat, uncited); } diff --git a/chrome/content/zotero/xpcom/quickCopy.js b/chrome/content/zotero/xpcom/quickCopy.js index 098034cdfa..bd259a5f55 100644 --- a/chrome/content/zotero/xpcom/quickCopy.js +++ b/chrome/content/zotero/xpcom/quickCopy.js @@ -279,8 +279,7 @@ Zotero.QuickCopy = new function() { else if (format.mode == 'bibliography') { items = items.filter(item => !item.isNote()); - // determine locale preference - var locale = format.locale ? format.locale : Zotero.Prefs.get('export.quickCopy.locale'); + var locale = _getLocale(format); // Copy citations if shift key pressed if (modified) { @@ -354,9 +353,21 @@ Zotero.QuickCopy = new function() { translator.cacheCode = true; await Zotero.Translators.getCodeForTranslator(translator); } + else if (format.mode === 'bibliography') { + let style = Zotero.Styles.get(format.id); + let locale = _getLocale(format); + // Cache CiteProc instances for HTML and text + style.getCiteProc(locale, 'html'); + style.getCiteProc(locale, 'text'); + } }; + function _getLocale(format) { + return format.locale || Zotero.Prefs.get('export.quickCopy.locale'); + } + + var _loadFormattedNames = Zotero.Promise.coroutine(function* () { var t = new Date; Zotero.debug("Loading formatted names for Quick Copy"); diff --git a/chrome/content/zotero/xpcom/style.js b/chrome/content/zotero/xpcom/style.js index 723d1bb164..971c91c0eb 100644 --- a/chrome/content/zotero/xpcom/style.js +++ b/chrome/content/zotero/xpcom/style.js @@ -39,6 +39,19 @@ Zotero.Styles = new function() { }; this.CSL_VALIDATOR_URL = "resource://zotero/csl-validator.js"; + + this._memoryPressureObserver = { + observe: (subject, topic) => { + if (topic !== 'memory-pressure') { + return; + } + for (let style of Object.values(this.getAll())) { + style.clearEngineCache(); + } + }, + QueryInterface: ChromeUtils.generateQI(['nsISupportsWeakReference']), + }; + Services.obs.addObserver(this._memoryPressureObserver, 'memory-pressure', /* ownsWeak */ true); /** @@ -201,7 +214,7 @@ Zotero.Styles = new function() { /** * Gets a style with a given ID * @param {String} id - * @param {Boolean} skipMappings Don't automatically return renamed style + * @param {Boolean} [skipMappings] Don't automatically return renamed style */ this.get = function (id, skipMappings) { if (!_initialized) { @@ -681,22 +694,39 @@ Zotero.Style = function (style, path) { if(this.source === this.styleID) { throw new Error("Style with ID "+this.styleID+" references itself as source"); } + + this._cachedEngines = new Map(); } /** * Get a citeproc-js CSL.Engine instance * @param {String} locale Locale code - * @param {String} format Output format one of [rtf, html, text] - * @param {Boolean} automaticJournalAbbreviations Whether to automatically abbreviate titles + * @param {String} [format] Output format one of [rtf, html, text] + * @param {Boolean} [automaticJournalAbbreviations] Whether to automatically abbreviate titles */ Zotero.Style.prototype.getCiteProc = function(locale, format, automaticJournalAbbreviations) { - if(!locale) { - var locale = Zotero.locale; - if(!locale) { - var locale = 'en-US'; - } - } + locale = locale || Zotero.locale || 'en-US'; format = format || 'text'; + automaticJournalAbbreviations = !!automaticJournalAbbreviations; + + let useCiteprocRs = Zotero.Prefs.get('cite.useCiteprocRs'); + + // We can cache the Engine instance if we aren't using citeproc-rs + // and this is an installed style + let cacheKey = !useCiteprocRs && this.path + ? JSON.stringify({ locale, format, automaticJournalAbbreviations }) + : null; + if (cacheKey && this._cachedEngines.has(cacheKey)) { + let engine = this._cachedEngines.get(cacheKey); + // Due to a bug in citeproc-js there are disambiguation issues after + // modifying items in Zotero. The lighter-weight rerebuildProcessorState() + // doesn't reset three properties of the processor (registry, tmp, and + // disambiguate) used for disambiguation, so we need to call the + // deprecated restoreProcessorState(), which resets everything. + // Revisit if restoreProcessorState() is removed. + engine.restoreProcessorState(); + return engine; + } // APA and some similar styles capitalize the first word of subtitles var uppercaseSubtitlesRE = /^apa($|-)|^academy-of-management($|-)|^(freshwater-science)/; @@ -763,7 +793,8 @@ Zotero.Style.prototype.getCiteProc = function(locale, format, automaticJournalAb try { var citeproc; - if (Zotero.Prefs.get('cite.useCiteprocRs')) { + var engineDesc; + if (useCiteprocRs) { citeproc = new Zotero.CiteprocRs.Engine( new Zotero.Cite.System({ automaticJournalAbbreviations, @@ -775,6 +806,7 @@ Zotero.Style.prototype.getCiteProc = function(locale, format, automaticJournalAb format == 'text' ? 'plain' : format, overrideLocale ); + engineDesc = 'CiteprocRs'; } else { citeproc = new Zotero.CiteProc.CSL.Engine( @@ -791,15 +823,27 @@ Zotero.Style.prototype.getCiteProc = function(locale, format, automaticJournalAb citeproc.opt.development_extensions.wrap_url_and_doi = true; // Don't try to parse author names. We parse them in itemToCSLJSON citeproc.opt.development_extensions.parse_names = false; + engineDesc = 'CSL'; } + // Cache the Engine instance if allowed + if (cacheKey) { + this._cachedEngines.set(cacheKey, citeproc); + Zotero.debug(`Caching ${engineDesc}.Engine instance with ${cacheKey} for ${this.styleID}`); + } + return citeproc; - } catch(e) { + } + catch (e) { Zotero.logError(e); throw e; } }; +Zotero.Style.prototype.clearEngineCache = function () { + this._cachedEngines.clear(); +}; + /** * Temporarily substitute `event-title` for `event` *