diff --git a/BibItNowMultiBrowser.zip b/BibItNowMultiBrowser.zip deleted file mode 100644 index 35e9d60..0000000 Binary files a/BibItNowMultiBrowser.zip and /dev/null differ diff --git a/MIGRATION.md b/MIGRATION.md new file mode 100644 index 0000000..78bd4fc --- /dev/null +++ b/MIGRATION.md @@ -0,0 +1,658 @@ +# MV2 → MV3 Migration Log + +This document tracks the migration of the Chrome build (`fullSource/BibItNowChrome/`) +from Manifest V2 to Manifest V3. Its purpose is to let a reviewer verify, without +re-deriving it from the diff alone, *why* each change was necessary and *why* it is +believed to preserve existing behavior. + +The pre-migration MV2 baseline is preserved at `origin/working-version-chrome` and at +commit `fb872f9` (the merge point this branch and `mv3-migration` both descend from). +Any commit here can be diffed against that point. + +## How to read this doc + +- **Chronological log** below — one entry per migration commit (or small group of + commits), backfilled from history and extended going forward. +- **Breaking-changes checklist** — the known set of MV2 patterns that MV3 disallows or + changes, tracked per-file so nothing gets migrated silently or missed. +- **Per-site test matrix** — see [MIGRATION_TEST_MATRIX.md](MIGRATION_TEST_MATRIX.md), + set up once core functionality was working end-to-end. Generated by + `fullSource/BibItNowChrome/generate_test_matrix.py`; see that script's docstring + before re-running it (not safe to blindly re-run once results are filled in). + +## Commit message convention + +Going forward, migration commits should be tagged in the subject line so a reviewer can +tell at a glance whether a commit is expected to be behavior-preserving or not: + +- `[SPLIT]` — purely mechanical reorganization (e.g. splitting a file in two, moving + code between files verbatim). No behavior change intended; should be near-trivial to + review by diffing the union of before/after against the original. +- `[MV3]` — a change driven by the MV3 platform migration itself (manifest keys, service + worker semantics, permission model, API replacements). May change runtime behavior or + introduce new failure modes (e.g. service worker suspension) and deserves closer + review. +- `[TEST]` — records manual verification results (updates to the test matrix), no source + change. + +Historical commits below predate this convention; each is annotated with the tag it +would carry under this scheme. + +--- + +## Breaking-changes checklist + +Status as audited on 2026-08-04. + +| # | MV2 pattern MV3 disallows/changes | Status | Notes | +|---|---|---|---| +| 1 | `manifest_version: 2` | ✅ Done (`dc4acf0`) | Bumped to 3. | +| 2 | `background.scripts` (persistent background page) | ✅ Done (`6ac1ed3`) | Replaced with `background.service_worker` (`30bbd0b`, `ce0d2f0`). See item 8 below — all original scripts now wired in. | +| 3 | `browser_action` key | ✅ Done (`30bbd0b`) | Renamed to `action`. | +| 4 | `options_page` key | ✅ Done (`30bbd0b`) | Renamed to `options_ui: { page, open_in_tab }`. | +| 5 | `_execute_browser_action` command | ✅ Done (`30bbd0b`) | Renamed to `_execute_action`. | +| 6 | Host patterns inside `optional_permissions` | ✅ Done (`30bbd0b`) | Moved to new `optional_host_permissions` key. | +| 7 | Programmatic script injection needs `scripting` permission | ✅ Done (`30bbd0b`) | Added to `permissions`. | +| 8 | Service worker can only load scripts via `importScripts` (classic worker) or `service_worker` entry point — MV2's flat `background.scripts` array has no direct MV3 equivalent | ✅ Done (`6ac1ed3`) | `service-worker.js` now imports all six original MV2 `background.scripts` entries: `background_url.js`, `background_load_core.js`, `background_load_resources.js`, `background_resources.js`, `background/parser/background_schema.js`, `background/parser/background_parse_bib.js` (`6b2a9f4`), and `background_interaction.js` (`6ac1ed3`). | +| 9 | Code inside background scripts that is not yet known to be MV3-safe | ✅ Done (`4b5b973`) | `background_load_resources.js` (split off in `758a34c`/`c36f89c`) used `XMLHttpRequest`, which doesn't exist in a service worker — this is what was actually causing "Service worker registration failed. Status code: 15" when first loaded unpacked. Rewritten to `fetch()`; see the `4b5b973` log entry for the two XML→JSON data-file conversions this required. | +| 10 | Module-level mutable state — a non-persistent (event-driven, suspendable) service worker can be torn down and restarted at any time, silently resetting top-level `var`/`let` state that MV2's persistent background page never lost | ✅ Done (`1d46fb5`) | `optionPageId`/`lastWorkingTabId` persisted to `chrome.storage.session` (rehydrated via a startup promise, `tabMemoryReady`, that `updateTab()`/`resetTabMemory()`/`openOptionPage()` wait on before touching either variable) — chosen because losing them silently breaks the `backNavigation` feature over a realistic idle window. Also re-registers `openOptionPage()`'s dynamic `chrome.tabs.onActivated` listener on startup when restored state says an option page tab is still open, since that registration doesn't itself persist. `parsedData`/`allowAutoCopy`/`allowAutoDownload`/`parsedDataContainer` deliberately left as plain, non-persisted state — worst case on restart is a harmless re-extraction or momentarily-stale cache, not a silently broken feature. Manually verified: opened the options page, force-terminated the service worker via `chrome://serviceworker-internals`, closed the options tab, confirmed focus switched back to the original working tab. | +| 11 | DOM APIs (`document`, `DOMParser`, `window`) — unavailable in a service worker | ✅ Audited, none found | Checked and clean: `background_url.js`, `background_load_core.js`, `background_load_resources.js`, `background_resources.js`, `background/parser/background_schema.js`, `background/parser/background_parse_bib.js`, `background_interaction.js` (fixed in `c3a55c7`, see item 15). | +| 12 | Blocking `webRequest`, synchronous XHR, `eval`/`new Function` — disallowed or restricted under MV3 | ✅ Audited, none found | Checked repo-wide. | +| 13 | Remote-hosted or inline ` + diff --git a/fullSource/BibItNowChrome/description.js b/fullSource/BibItNowChrome/description.js new file mode 100644 index 0000000..3087eb1 --- /dev/null +++ b/fullSource/BibItNowChrome/description.js @@ -0,0 +1,1499 @@ +const BINOptionPage = ( function () { + + //pointer to option objects + var generalOptions = null, displayOptions = null, redirectionSchemes = null, redirectionSchemeFormatStrings = null, citationFormats = null, previousBibkeyFormatString = "", allowFileExport = true; + + //function to set options. + function setOptions(options, mode = 0) { + if (options == null || typeof(options) != 'object') return; + let opts = options.generalOptions; + + if (opts != null && typeof(opts) == 'object') { + generalOptions = opts; + previousBibkeyFormatString = generalOptions.bibkeyFormatString; + } + opts = options.displayOptions; + if (opts != null && typeof(opts) == 'object') displayOptions = opts; + opts = options.redirectionSchemes; + if (opts != null && typeof(opts) == 'object') { + redirectionSchemes = opts; + opts = options.redirectionSchemeFormatStrings; + if (opts != null && Array.isArray(opts)) { + redirectionSchemeFormatStrings = opts; + } else { + redirectionSchemes = null; + } + } + opts = options.citationFormats; + if (opts != null && typeof(opts) == 'object') { + citationFormats = opts; + } + queryOptionalPermissions(0); + } + + //function to set general option in background + function setOption(option,value,rebuildMode = 1, msgType = "update_options_background", handler = doNothing) { + if (generalOptions != null) { + // set option + if (option != "bibkeyFormatString") generalOptions[option] = value; + + // rebuild option page + rebuildOptionPage(rebuildMode); + + //update options in background + const updateMessage = { msgType: msgType , options: {} }; + updateMessage.options[option] = value; + sendMsg(updateMessage, handler); + } + } + + //function to set display option in background + function setDisplayOption(parseMode, option, value) { + if (generalOptions != null) { + // set option + displayOptions.optionArray[parseMode][option] = value; + + // rebuild option page + rebuildOptionPage(1); + + //update options in background + const updateMessage = { msgType: "update_display_options_background" , parseMode: parseMode , options: {} }; + updateMessage.options[option] = value; + sendMsg(updateMessage, doNothing); + } + } + + // function to update redirection scheme after handling in background + function updateRedirectionScheme(response) { + if (redirectionSchemes != null && redirectionSchemeFormatStrings != null && response != null && typeof(response) == 'object' && response.currentScheme != null) { + + //update local object with global properties + redirectionSchemes.currentScheme = response.currentScheme; + + //replace, add or remove + let index = response.index; + const scheme = response.scheme, formatString = response.formatString; + if (response.removeAll == true) { + //reset schemes + redirectionSchemes.schemes = []; + redirectionSchemeFormatStrings = []; + + //rebuild option page + rebuildOptionPage(4,-1); + + } else if ( scheme != null && Array.isArray(scheme) ) { + + //add all from scheme array + const numSchemes = scheme.length, schemes = redirectionSchemes.schemes; + index = schemes.length; + for (let i = 0; i -1) { + if (scheme == null && index < redirectionSchemes.schemes.length) { + //remove + redirectionSchemes.schemes.splice(index,1); + redirectionSchemeFormatStrings.splice(index,1); + + //set new scheme to select + index--; + + //rebuild option page + rebuildOptionPage(4,index,true); + } else { + //replace or add + redirectionSchemes.schemes[index] = scheme; + redirectionSchemeFormatStrings[index] = formatString; + + //rebuild option page + rebuildOptionPage(4,index); + } + } + } + } + + //function to export redirection scheme to local json file + function exportRedirectionScheme(scheme,name = "RedirectionSchemes.bnrs") { + + //early out if not allowed + if (!allowFileExport) return; + + //sanitize name + if (name == null || typeof(name) != 'string') { + name = "RedirectionSchemes.bnrs"; + } else { + name = name.replace(/[\s]*/g,""); + if (name.search(/\.bnrs$/) == -1) name += ".bnrs"; + } + + //check if json-compatible + try { + scheme = JSON.stringify(scheme); + } catch(error) { + scheme = null; + } + + //if successfully parsed, proceed + if (scheme != null && typeof(scheme) == 'string' && scheme.length > 0) { + + //disable export buttons/links + allowFileExport = false; + + //create an iframe for correct file name (what the hell, why do webextensions not provide a proper settings export function???) + const frame = document.createElement("iframe"); + frame.srcdoc = 'Option Export'; + frame.style.position = "fixed"; + frame.style.top = "-999px"; + frame.style.height = "1px"; + frame.onload = function() { + let cWin = frame.contentWindow; + let anch = cWin.document.getElementById("schemeDownloader"); + anch.href = cWin.URL.createObjectURL(new cWin.Blob([scheme], { type: "text/bnrs"})); + anch.click(); + setTimeout( function() { + frame.remove(); + allowFileExport = true; + }, 1000 + ); + + }; + document.body.appendChild(frame); + } + } + + //function to export all redirection schemes + function exportRedirectionSchemes() { + //early out + if (redirectionSchemes == null || typeof(redirectionSchemes) != 'object' || redirectionSchemeFormatStrings == null || !Array.isArray(redirectionSchemeFormatStrings)) return; + + //get scheme array + const schemes = redirectionSchemes.schemes; + + //early out if no array + let length = 0; + if (schemes == null || !Array.isArray(schemes) || (length = schemes.length) < 1) return; + + //create copy of scheme array + schemesToExport = []; + + //fill with content of scheme array and formatString array + for (let i = 0; i 0) exportRedirectionScheme(schemesToExport); + } + + //function to remove all redirection schemes + function removeRedirectionSchemes() { + //early out + if (redirectionSchemes == null || typeof(redirectionSchemes) != 'object' || redirectionSchemeFormatStrings == null || !Array.isArray(redirectionSchemeFormatStrings) || redirectionSchemeFormatStrings.length < 1) return; + + //tell background to remove all schemes if user is sure + if (confirm("Are you sure you want to REMOVE ALL redirection schemes?")) { + setRedirectionScheme(0, null, true); + } + } + + //function to open redirection scheme file + function openRedirectionSchemeFile() { + //early out + if (redirectionSchemes == null || typeof(redirectionSchemes) != 'object' || redirectionSchemeFormatStrings == null || !Array.isArray(redirectionSchemeFormatStrings)) return; + + //trigger open file dialogue + document.getElementById('importedRedirectionSchemes').click(); + } + + function importRedirectionSchemes(fileContent) { + + //early out + if (fileContent == null || typeof(fileContent) != 'string' || fileContent.length == 0 || redirectionSchemes == null || typeof(redirectionSchemes) != 'object' || redirectionSchemeFormatStrings == null || !Array.isArray(redirectionSchemeFormatStrings)) return; + + //trigger open file dialogue + try { + fileContent = JSON.parse(fileContent); + } catch(exception) { + fileContent = null; + } + + //set new redirection schemes if available + if (fileContent != null && Array.isArray(fileContent) && fileContent.length > 0) { + setRedirectionScheme(-1,fileContent,false); + } + } + + // function to update citation format after handling in background + function updateCitationFormat(response) { + if (citationFormats != null && response != null && typeof(response) == 'object') { + + //if csl, do nothing TODO change + if (response.isCSL) { + document.getElementById("citationConvertedCSL").value = (response.convertedCSL != null) ? "successfully" : "failed"; + return; + } + + //set index + let index = response.index, removeAll = (response.removeAll == true); + + //set current format + citationFormats.currentFormat = index; + if (!(response.format != null && response.currentFormatSource == null)) { + if (index > -1 || removeAll) citationFormats.currentFormatSource = response.currentFormatSource; + } + + //replace, add or remove + const format = response.format; + if (response.removeAll == true) { + //reset schemes + citationFormats.formats = []; + + //rebuild option page + rebuildOptionPage(5,-1); + + } else if ( format != null && Array.isArray(format) ) { + + //add all from format array + const numFormats = format.length, formats = citationFormats.formats; + index = formats.length; + for (let i = 0; i -1) { + if (format == null && index < citationFormats.formats.length) { + //remove + citationFormats.formats.splice(index,1); + + //shift index + index--; + + //rebuild option page + rebuildOptionPage(5,index,false,true); + } else { + + //replace or add + citationFormats.formats[index] = format; + + //rebuild option page + rebuildOptionPage(5,index); + } + } + } + } + + //function to export citation format to local json file + function exportCitationFormat(format,name = "CitationFormat.bnfa") { + + //early out if not allowed + if (!allowFileExport) return; + + //sanitize name + if (name == null || typeof(name) != 'string') { + name = "CitationFormat.bnfa"; + } else { + name = name.replace(/[\s]*/g,""); + if (name.search(/\.bnfa$/) == -1) name += ".bnfa"; + } + + //check if json-compatible + try { + format = JSON.stringify(format,null, '\t'); + } catch(error) { + format = null; + } + + //if successfully parsed, proceed + if (format != null && typeof(format) == 'string' && format.length > 0) { + + //disable export buttons/links + allowFileExport = false; + + //create an iframe for correct file name (what the hell, why do webextensions not provide a proper settings export function???) + const frame = document.createElement("iframe"); + frame.srcdoc = 'Option Export'; + frame.style.position = "fixed"; + frame.style.top = "-999px"; + frame.style.height = "1px"; + frame.onload = function() { + let cWin = frame.contentWindow; + let anch = cWin.document.getElementById("formatDownloader"); + anch.href = cWin.URL.createObjectURL(new cWin.Blob([format], { type: "text/bnfa"})); + anch.click(); + setTimeout( function() { + frame.remove(); + allowFileExport = true; + }, 1000 + ); + + }; + document.body.appendChild(frame); + } + } + + //function to export all citation formats + function exportCitationFormats(formatSources) { + //early out + if (citationFormats == null || typeof(citationFormats) != 'object') return; + + //early out if no valid response + if (formatSources == null || typeof(formatSources) != 'object') return; + + //get format sources from response + formatSources = formatSources.formatSource; + let length = 0; + if (formatSources == null || !Array.isArray(formatSources) || (length = formatSources.length) < 1) return; + + //get format array + const formats = citationFormats.formats; + + //early out if no array + if (formats == null || !Array.isArray(formats) || length != formats.length) return; + + //create copy of format array + formatsToExport = []; + + //fill with content of format array and formatString array + for (let i = 0; i 0) exportCitationFormat(formatsToExport); + } + + //function to remove all custom citation formats + function removeCitationFormats() { + //early out + if (citationFormats == null || typeof(citationFormats) != 'object') return; + + //tell background to remove all schemes if user is sure + if (confirm("Are you sure you want to REMOVE ALL CUSTOM citation formats?")) { + setCitationFormat(0, null, true); + } + } + + //function to open citation format file + function openCitationFormatFile() { + //early out + if (citationFormats == null || typeof(citationFormats) != 'object') return; + + //trigger open file dialogue + document.getElementById('importedCitationFormats').click(); + } + + //function to handle citation format imported from file + function importCitationFormats(fileContent) { + + //early out + if (fileContent == null || typeof(fileContent) != 'string' || fileContent.length == 0 || citationFormats == null || typeof(citationFormats) != 'object') return; + + //trigger open file dialogue + try { + fileContent = JSON.parse(fileContent); + } catch(exception) { + fileContent = null; + } + + //set new citation formats if available + let numFormats = citationFormats.formats.length; + let numFormatsToAdd = 0; + if (fileContent != null && Array.isArray(fileContent) && (numFormatsToAdd = fileContent.length) > 0) { + for (let i = 0; i 0) { + if (generalOptions != null) { + previousBibkeyFormatString = generalOptions.bibkeyFormatString; + generalOptions.bibkeyFormatString = formatString; + } + document.getElementById('bibkeyFormatString').value = formatString; + document.getElementById('bibkeyExample').innerText = ""; + } else if ((formatString = format.exampleBibkey) != null && typeof(formatString) == 'string' && formatString.length > 0) { + document.getElementById('bibkeyExample').innerText = formatString; + } else { + document.getElementById('bibkeyExample').innerText = ""; + } + } + } + + //function to set dynamic citation download timeout + function setCitationDownloadTimeout(timeoutInSeconds) { + if (generalOptions != null) { + //set timeout. Note that timeout is milliseconds internally while the box is in seconds + if (timeoutInSeconds > 99) timeoutInSeconds = 99; + const timeoutInMilliseconds = timeoutInSeconds > 0 ? timeoutInSeconds*1000 : 1000; + generalOptions["dyn_download_timeout"] = timeoutInMilliseconds; + + //udpate timeout box + document.getElementById('downloadCitationTimeout').value = timeoutInMilliseconds/1000; + + //update options in background + const updateMessage = { msgType: "update_options_background" , options: {} }; + updateMessage.options["dyn_download_timeout"] = timeoutInMilliseconds; + sendMsg(updateMessage, doNothing); + } + } + + // function to rebuild option page, mode == 0 is completely resetting it, mode == 1 only for disabling/enabling timeout box + function rebuildOptionPage(mode,index = -1,forceRedirectionSchemeRebuild = false, forceCitationFormatRebuild = false) { + + //early out + if (mode < 0) return; + + // determine whether citation download is activated + let downloadCitation = generalOptions["dyn_download"]; + + //enable/disable timeout box depending on whether citation download is activated + let doIt = downloadCitation; + let timeOutBox = document.getElementById('downloadCitationTimeout'); + timeOutBox.disabled = !doIt; + + //get state of focus color checkbox + doIt = generalOptions["set_focus_color"]; + + //enable/disable color picker + let focusColor = generalOptions["focus_color"]; + if (focusColor == null || focusColor == undefined) focusColor = "#a1e9ff"; + let colorPicker = document.getElementById('focusColorPicker'); + if (colorPicker != null) { + colorPicker.disabled = !doIt; + colorPicker.value = doIt ? focusColor : "#F2F1F0"; + } + + //enable/disable text color picker + document.getElementById('focusTextColorCheckboxText').style.color = doIt ? "black" : "gray"; + focusColor = generalOptions["focus_text_color"]; + if (focusColor == null || focusColor == undefined) focusColor = "#000000"; + colorPicker = document.getElementById('focusTextColorPicker'); + if (colorPicker != null) { + colorPicker.disabled = !doIt; + colorPicker.value = doIt ? focusColor : "#F2F1F0"; + } + + //0 = first load, update everything + if (mode == 0) { + + //set citation download checkboxes + document.getElementById('downloadCitationCheckbox').checked = downloadCitation; + document.getElementById('downloadCitationNatureCheckbox').checked = generalOptions["allowNatureSpringer"]; + document.getElementById('downloadCitationNatureMessageCheckbox').checked = generalOptions["showNatureSpringerMessage"]; + activateDynDownloadOptions(downloadCitation); + + //set pdf fallback + downloadCitation = generalOptions["pdfFallback"]; + document.getElementById('pdfFallbackCheckbox').checked = downloadCitation; + document.getElementById('pdfFallbackAutoCheckbox').checked = generalOptions["pdfFallbackAutomatic"]; + activatePdfFallbackOptions(downloadCitation); + + //set value of timeout box + timeOutBox.value = (generalOptions["dyn_download_timeout"])/1000; + + //set autofocus and autocopy checkbox + document.getElementById('autofocusCheckbox').checked = generalOptions["text_autofocus"]; + + //set focus color checkboxes + document.getElementById('focusColorCheckbox').checked = doIt; + document.getElementById('focusTextColorCheckboxText').style.color = doIt ? "black" : "gray"; + + //set autofocus checkbox + document.getElementById('backNavigationCheckbox').checked = generalOptions["backNavigation"]; + + //set "hide-link" checkboxes + document.getElementById('copyHideCheckbox').checked = generalOptions["hide_copy_option_link"]; + document.getElementById('formatHideCheckbox').checked = generalOptions["hide_custom_format_link"]; + document.getElementById('editHideCheckbox').checked = generalOptions["hide_redirection_scheme_link"]; + + //set bobbels checkbox + document.getElementById('bobbelsCheckbox').checked = generalOptions["bobbels"]; + + //set copy workflow checkboxes + doIt = generalOptions["text_autocopy"]; + document.getElementById('copyEnableCheckbox').checked = doIt; + document.getElementById('copyLinkCheckbox').checked = generalOptions["text_autocopy_link"]; + document.getElementById('copyLinkOnChangeCheckbox').checked = generalOptions["text_autocopy_link_on_change"]; + document.getElementById('copyVisualCueCheckbox').checked = generalOptions["text_autocopy_visual"]; + document.getElementById('copyButtonLinkCheckbox').checked = generalOptions["copy_button_link"]; + document.getElementById('copyAlwaysEnableCheckbox').checked = generalOptions["text_autocopy_always"]; + activateAutocopyOptions(doIt); + + //hide or display auto copy options depending on availability + try { + const copyOptionDiv = document.getElementById("autocopyCheckboxes"); + if (navigator != undefined && navigator.clipboard != null && permRef != null) { + copyOptionDiv.style.removeProperty("display"); + } else { + copyOptionDiv.style["display"] = "none"; + } + } catch(error) { + copyOptionDiv.style["display"] = "none"; + } + + //set url rewrite adjuster, match insensitivity and url suppression checkbox + doIt = generalOptions["urlCorrection"]; + activateURLRewriteUI(doIt); + document.getElementById('urlCorrectionCheckbox').checked = doIt; + document.getElementById('delimInsensitiveCheckbox').checked = generalOptions["urlCorrectionInsensitive"]; + document.getElementById('suppressUrlCheckbox').checked = generalOptions["suppressUrl"]; + doIt = generalOptions["urlCorrectionScheme"]; + if (doIt == null || typeof(doIt) != 'string' || doIt == "") doIt = "none"; + document.getElementById('ignoredURL').innerText = doIt; + + //set bibkey formatter ui + doIt = generalOptions["bibkeyFormatting"]; + activateBibkeyFormatterUI(doIt); + document.getElementById('bibkeyCheckbox').checked = doIt; + document.getElementById('bibkeyFormatString').value = generalOptions["bibkeyFormatString"]; + document.getElementById('whiteSpaceChar').value = generalOptions["bibkeyWhiteSpace"]; + + //set redirection scheme and citation format ui + updateRedirectionSchemeUI(redirectionSchemes.schemes.length,redirectionSchemes.currentScheme,true); + updateCitationFormatUI(citationFormats.formats.length,citationFormats.currentFormat,true); + } else if (mode == 2) { + activateURLRewriteUI(generalOptions["urlCorrection"]); + } else if (mode == 3) { + activateBibkeyFormatterUI(generalOptions["bibkeyFormatting"]); + } else if (mode == 4) { + //just update redirection schemes + updateRedirectionSchemeUI(redirectionSchemes.schemes.length,index,forceRedirectionSchemeRebuild); + } else if (mode == 5) { + + //just update citation formats + updateCitationFormatUI(citationFormats.formats.length,index,forceCitationFormatRebuild); + } else if (mode == 6) { + activateAutocopyOptions(generalOptions["text_autocopy"]); + } + } + + // function to reset copy option UI elements + function activateAutocopyOptions(allowAutocopy) { + + //enable/disable URL rewrite adjuster + let elems = ['copyAlwaysEnableCheckboxText','copyLinkCheckboxText','copyAlwaysIntroText','copyLinkIntroText','copyLinkOnChangeIntroText','copyLinkOnChangeCheckboxText']; + let length = elems.length; + for (let i = 0; i 0) { + //only update indicated element if index != -1 and rebuild not forced, otherwise rebuild + if (index > -1 && index < activate && !forceRebuild) { + + let format = citationFormats.formats[index]; + + //check if element needs to be added to selector box + if (index < formatSelector.childNodes.length) { + //if not, update name in selector box + let option = formatSelector.childNodes[index]; + if (format.name != option.innerText) option.innerText = format.name; + } else { + //if yes, add to end of selector box and select + let option = document.createElement("option"); + option.value = index; option.innerText = format.name; + formatSelector.appendChild(option); + } + + //update ui elements + showFormatOptions(format,citationFormats.currentFormatSource, index); + } else { + //completely rebuild selector box + while (formatSelector.hasChildNodes()) { + formatSelector.removeChild(formatSelector.childNodes[0]); + } + let formats = citationFormats.formats; + for (let i = 0; i 1) { + whiteSpaceChar = whiteSpaceChar.slice(0,1); + } + showAsTooltip = (showAsTooltip != null && typeof(showAsTooltip) == 'boolean' && showAsTooltip == true); + + //populate UI elements + let elems = ['redirectionSchemeName','redirectionSchemeTooltip','redirectionSchemeFormatString','whiteSpaceCharRedirection']; + let values = [name,tooltip,formatString,whiteSpaceChar]; + for (let i = 0; i 0) { + + //only update indicated element if index != -1 and rebuild not forced, otherwise rebuild + if (index > -1 && index < activate && !forceRebuild) { + let scheme = redirectionSchemes.schemes[index]; + + //check if element needs to be added to selector box + if (index < schemeSelector.childNodes.length) { + //if not, update name in selector box + let option = schemeSelector.childNodes[index]; + if (scheme.name != option.innerText) option.innerText = scheme.name; + } else { + //if yes, add to end of selector box and select + let option = document.createElement("option"); + option.value = index; option.innerText = scheme.name; + schemeSelector.appendChild(option); + } + + //update ui elements + showSchemeOptions(scheme.name,scheme.tooltip,redirectionSchemeFormatStrings[index],scheme.showAsTooltip,scheme.whiteSpaceReplacement, index); + } else { + //completely rebuild selector box + while (schemeSelector.hasChildNodes()) { + schemeSelector.removeChild(schemeSelector.childNodes[0]); + } + let schemes = redirectionSchemes.schemes; + for (let i = 0; i 0) { + if (confirmation[i] || confirm("Are you sure you want to CHANGE the custom bibkey format?")) { + setOption("bibkeyFormatString",text,1,msgTypes[i], + function(format) { + //set bibkey format + if (format != null) setBibkeyFormat(format); + } + ); + } + } + } + ); + } + } + //add listener to "restore bibkey format string"-button + document.getElementById('restoreBibkeyString').addEventListener('click',function(event) { + + // set text from bibkey format string box + if (previousBibkeyFormatString != null && previousBibkeyFormatString.length > 0) { + document.getElementById('bibkeyFormatString').value = previousBibkeyFormatString; + } + } + ); + + //add listener to "show current bibkey format string"-button + document.getElementById('showCurrentBibkeyString').addEventListener('click',function(event) { + // get text from bibkey format string box + document.getElementById('bibkeyFormatString').value = generalOptions.bibkeyFormatString; + } + ); + + //add listener to "show default bibkey format string"-button + document.getElementById('restoreBibkeyDefault').addEventListener('click',function(event) { + // get text from bibkey format string box + document.getElementById('bibkeyFormatString').value = "{!authors[0,0]}{year||access_year}{month_abbrev}"; + } + ); + + //add listener to "show format string rules"-button + document.getElementById('showBibkeyInfo').addEventListener('click',function(event) { + // figure out if bibkeyInfo is hidden or not + const bibkeyInfo = document.getElementById('bibkeyInfo'); + if (bibkeyInfo.style.display == "none") { + bibkeyInfo.style.display = "inline"; + this.value = "Hide full reference"; + } else { + bibkeyInfo.style.display = "none"; + this.value = "Show full reference"; + } + } + ); + + //add listener to "show delimiter insensitive matching info"-button + document.getElementById('showDelimInsensitiveInfo').addEventListener('click',function(event) { + // figure out if delimInsensitiveInfo is hidden or not + const delimInsensitiveInfo = document.getElementById('delimInsensitiveInfo'); + if (delimInsensitiveInfo.style.display == "none") { + delimInsensitiveInfo.style.display = "inline"; + this.value = "Hide what is this about"; + } else { + delimInsensitiveInfo.style.display = "none"; + this.value = "Show what is this about"; + } + } + ); + + //add listener to add scheme link + document.getElementById('addRedirectionScheme').addEventListener('click',function(event) { + setRedirectionScheme(redirectionSchemes.schemes.length, { name: "Custom" , tooltip: "Custom tooltip" , formatString: "None", whiteSpaceReplacement: "+" , showAsTooltip: false}); + } + ); + + //add listener to delete scheme button + document.getElementById('redirectionSchemeRemove').addEventListener('click',function(event) { + //get index of selected scheme + const selector = document.getElementById('redirectionSelectorBox'); + index = parseInt(selector.options[selector.selectedIndex].value); + + //tell background to remove scheme if user is sure + if (confirm("Are you sure you want to REMOVE the redirection scheme \"" + redirectionSchemes.schemes[index].name + "\"?")) { + setRedirectionScheme(index, null); + } + } + ); + + //add listener to save scheme button + document.getElementById('redirectionSchemeSave').addEventListener('click',function(event) { + //get index of selected scheme + const selector = document.getElementById('redirectionSelectorBox'); + index = parseInt(selector.options[selector.selectedIndex].value); + + //tell background to change scheme if user is sure + const name = document.getElementById('redirectionSchemeName'); + if (name.value.replace(/[\s]+/g,"") == "") name.value = "Custom"; + if (confirm("Are you sure you want to CHANGE the redirection scheme \"" + redirectionSchemes.schemes[index].name + "\"?")) { + setRedirectionScheme(index, { name: name.value , tooltip: document.getElementById('redirectionSchemeTooltip').value , formatString: document.getElementById('redirectionSchemeFormatString').value, whiteSpaceReplacement: document.getElementById('whiteSpaceCharRedirection').value, showAsTooltip: (document.getElementById('redirectionSchemeAppearance2').checked == true)}); + } + } + ); + + //add listener to export scheme button + document.getElementById('redirectionSchemeExport').addEventListener('click',function(event) { + //call export function + const name = document.getElementById('redirectionSchemeName'); + if (name.value.replace(/[\s]+/g,"") == "") name.value = "Custom"; + exportRedirectionScheme([ { name: name.value , tooltip: document.getElementById('redirectionSchemeTooltip').value , formatString: document.getElementById('redirectionSchemeFormatString').value, whiteSpaceReplacement: document.getElementById('whiteSpaceCharRedirection').value, showAsTooltip: (document.getElementById('redirectionSchemeAppearance2').checked == true)} ] , name.value); + } + ); + + //add listeners to example scheme downloads + document.getElementById('exportGoogleScholarScheme').addEventListener('click',function(event) { + //call export function + exportRedirectionScheme([ {name: "GScholar",tooltip: "Find article on Google Scholar", formatString: "{doi\\https://scholar.google.com/scholar_lookup?&doi=\\}{title\\&title=\\}",whiteSpaceReplacement: "+",showAsTooltip: true} ] , "GScholar"); + } + ); + //add listeners to example scheme downloads + document.getElementById('exportGoogleBooksScheme').addEventListener('click',function(event) { + //call export function + exportRedirectionScheme([ {name: "GBooks", tooltip: "Find ISBN on Google Books",formatString: "{!isbn\\https://www.google.com/search?tbo=p&tbm=bks&q=isbn:\\&num=10\\/[\\-]//gi/}", whiteSpaceReplacement: "+", showAsTooltip: true} ] , "GBooks"); + } + ); + + + //add listeners to export/import/remove-all schemes links + { + const functions = [exportRedirectionSchemes,removeRedirectionSchemes,openRedirectionSchemeFile]; + const buttons = ['exportRedirectionSchemes','removeRedirectionSchemes','importRedirectionSchemes']; + const numFuncs = functions.length; + for (let i = 0; i