Last active
August 31, 2024 11:59
-
-
Save dschaehi/7dae3136ad126ff66de9c88efc511ab2 to your computer and use it in GitHub Desktop.
Actions and Tags for Zotero
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| type: ActionsTagsBackup | |
| author: jaeheelee | |
| platformVersion: 7.0.3 | |
| pluginVersion: 2.0.0 | |
| timestamp: '2024-08-31T11:59:12.270Z' | |
| actions: | |
| 1715690567316-0F2mzqBj: | |
| event: 0 | |
| operation: 4 | |
| data: > | |
| /** | |
| * Toggle Right Pane | |
| * @author [MuiseDestiny](https://github.com/MuiseDestiny), windingwind | |
| * @usage Assign a shortcut | |
| * @link https://github.com/windingwind/zotero-actions-tags/discussions/169 | |
| * @see https://github.com/windingwind/zotero-actions-tags/discussions/169 | |
| */ | |
| const Zotero = require("Zotero"); | |
| const Zotero_Tabs = require("Zotero_Tabs"); | |
| const document = require("document"); | |
| const DEFAULT_OPEN_PANE = "item"; | |
| if (item) { | |
| return; | |
| } | |
| const splitter = document.querySelector(Zotero_Tabs.selectedType === | |
| "library" ? "#zotero-items-splitter":"#zotero-context-splitter"); | |
| if (splitter.getAttribute("state") == "collapsed") { | |
| splitter.setAttribute("state", ""); | |
| return; | |
| } else { | |
| splitter.setAttribute("state", "collapsed"); | |
| return; | |
| } | |
| shortcut: ⇧,⌃,arrowright | |
| enabled: true | |
| menu: '' | |
| name: Toggle Right Pane | |
| showInMenu: | |
| item: false | |
| collection: false | |
| tools: false | |
| reader: false | |
| readerAnnotation: false | |
| 1715690499454-0F2mzqBj: | |
| event: 0 | |
| operation: 4 | |
| data: |- | |
| /** | |
| * Toggle Left Pane | |
| * @author [MuiseDestiny](https://github.com/MuiseDestiny), windingwind | |
| * @usage Assign a shortcut | |
| * @link https://github.com/windingwind/zotero-actions-tags/discussions/169 | |
| * @see https://github.com/windingwind/zotero-actions-tags/discussions/169 | |
| */ | |
| const Zotero = require("Zotero"); | |
| const Zotero_Tabs = require("Zotero_Tabs"); | |
| const document = require("document"); | |
| if (item) { | |
| return; | |
| } | |
| if (Zotero_Tabs.selectedType === "library") { | |
| const splitter = document.querySelector("#zotero-collections-splitter"); | |
| if (splitter.getAttribute("state") == "collapsed") { | |
| splitter.setAttribute("state", ""); | |
| return; | |
| } else { | |
| splitter.setAttribute("state", "collapsed"); | |
| return; | |
| } | |
| } else { | |
| Zotero.Reader.getByTabID( | |
| Zotero_Tabs.selectedID | |
| )._internalReader.toggleSidebar(); | |
| return; | |
| } | |
| shortcut: ⇧,⌃,ArrowLeft | |
| enabled: true | |
| menu: '' | |
| name: Toggle Left Pane | |
| showInMenu: | |
| item: false | |
| collection: false | |
| tools: false | |
| reader: false | |
| readerAnnotation: false | |
| 1707936562886-0F2mzqBj: | |
| event: 0 | |
| operation: 4 | |
| data: >- | |
| const ZoteroPane = require("ZoteroPane"); | |
| var iv = ZoteroPane.itemsView; await | |
| iv.selectItem(iv.getRow(Zotero.Utilities.rand(0, iv.rowCount - 1)).id) | |
| shortcut: ⌘,r | |
| enabled: true | |
| menu: Select random item | |
| name: Select random item | |
| showInMenu: | |
| item: false | |
| collection: false | |
| tools: true | |
| reader: false | |
| readerAnnotation: false | |
| default1: | |
| event: 3 | |
| operation: 2 | |
| data: /unread | |
| shortcut: '' | |
| enabled: false | |
| menu: '' | |
| name: Remove Unread When Close Tab | |
| showInMenu: | |
| item: true | |
| collection: true | |
| tools: true | |
| reader: true | |
| readerAnnotation: true | |
| 1707922271773-0F2mzqBj: | |
| event: 0 | |
| operation: 4 | |
| data: >- | |
| // Remove tags | |
| // @author Yang1824 | |
| // @link | |
| https://github.com/windingwind/zotero-actions-tags/discussions/127 | |
| // @usage Shortcut | |
| if (!item) return; | |
| item.getTags().map((tag) => tag.tag).forEach((tag) => | |
| {item.removeTag(tag)}) | |
| shortcut: ⇧,⌃,d | |
| enabled: true | |
| menu: Remove tags | |
| name: Remove tags | |
| showInMenu: | |
| item: false | |
| collection: false | |
| tools: false | |
| reader: false | |
| readerAnnotation: false | |
| 1707944365108-0F2mzqBj: | |
| event: 0 | |
| operation: 4 | |
| data: "/**\n * Relate selected items\n * @author windingwind\n * @usage Set all selected items to be related to each other from a right-click on several items\n * @link https://github.com/windingwind/zotero-actions-tags/discussions/164\n * @see https://github.com/windingwind/zotero-actions-tags/discussions/164\n */\nif (items?.length === 0 || item) {\n\treturn;\n}\n\n// https://github.com/wshanks/Zutilo/blob/8d53047cf35c11490e0d82156d4ee12136c7fb31/addon/chrome/content/zutilo/zoteroOverlay.js#L710\nconst zitems = items.filter(_item => _item.isRegularItem() || _item.isNote() || _item.isAttachment());\nif (zitems.length < 2) {\n\treturn \"Must select 2 or more items\";\n}\n\nfor (let zitem of zitems) {\n\tfor (let addItem of zitems) {\n\t\tif (zitem != addItem) {\n\t\t\tzitem.addRelatedItem(addItem)\n\t\t}\n\t}\n\tzitem.saveTx();\n}\n\nreturn `Successfully relate ${zitems.length} items.`;" | |
| shortcut: ⇧,⌃,l | |
| enabled: true | |
| menu: Related selected items | |
| name: Relate selected items | |
| showInMenu: | |
| item: true | |
| collection: false | |
| tools: false | |
| reader: false | |
| readerAnnotation: false | |
| 1711397904537-0F2mzqBj: | |
| event: 9 | |
| operation: 4 | |
| data: "const ZoteroPane = require(\"ZoteroPane\");\nconst Zotero_Tabs = require(\"Zotero_Tabs\");\nconst document = require(\"document\");\n\nasync function getAttachmentPath(item) {\n if (item.isAttachment() && !item.isNote()) {\n return await item.getFilePathAsync();\n }\n else if (item.isRegularItem() && !item.isAttachment()) {\n let attachments = await item.getAttachments();\n for (let attachmentID of attachments) {\n let attachment = await Zotero.Items.getAsync(attachmentID);\n\t\t\treturn await attachment.getFilePathAsync();\n }\n }\n return null;\n}\n\nasync function openFile(item) {\n let filePath = await getAttachmentPath(item);\n if (!filePath) {\n return false;\n }\n // macbook path : -p /usr/bin/qlmanage\n // linux path : /usr/bin/sushi\n let applicationPath = \"/usr/bin/qlmanage\"\n\tlet args = ['-p', filePath]\n\tZotero.Utilities.Internal.exec(applicationPath, args);\n}\n\nasync function oneKey(event) {\n\tvar key = String.fromCharCode(event.which);\n\tlet item = ZoteroPane.getSelectedItems()[0];\n\tif (Zotero_Tabs.selectedID === \"zotero-pane\") {\n\t\tif ((key == ' ' && !(event.ctrlKey || event.altKey || event.metaKey)) || (key == 'y' && event.metaKey && !(event.ctrlKey || event.altKey))) {\n\t\t\tawait openFile(item);\n\t\t\tZoteroPane.collectionsView.selectItems([item.id]);\n\t\t\treturn \n\t\t}\n\t}\n}\n\ndocument.getElementById('zotero-items-tree').addEventListener(\"keydown\", oneKey, false);" | |
| shortcut: '' | |
| enabled: true | |
| menu: QuickLook | |
| name: QuickLook | |
| showInMenu: | |
| item: false | |
| collection: false | |
| tools: false | |
| reader: false | |
| readerAnnotation: false | |
| 1710970507615-0F2mzqBj: | |
| event: 0 | |
| operation: 4 | |
| data: "/**\n * QuickCopy items\n * @author Zotero Community\n * @usage Copy selected items to the clipboard\n * @link https://github.com/windingwind/zotero-actions-tags/discussions/165\n * @see https://github.com/windingwind/zotero-actions-tags/discussions/165\n */\nif (items?.length === 0 || item) {\n\treturn;\n}\n\nconst Zotero = require(\"Zotero\");\nconst ZoteroPane = require(\"ZoteroPane\");\n\nconst pref = 'export.quickCopy.setting';\nconst origSetting = Zotero.Prefs.get(pref);\n\n/**\n * Replace this line with your own setting\n * Steps:\n * 1. Change Settings -> Export -> Quick Copy -> Item Format\n * 2. Open Settings -> Advanced -> Config Editor\n * 3. Search for `export.quickCopy.setting`.\n * 4. Paste the value and replace the string `bibliography/html=http://www.zotero.org/styles/acm-siggraph` below.\n * 5. Reset Settings -> Export -> Quick Copy -> Item Format (If necessary)\n */\nconst newSetting = \"export=ca65189f-8815-4afe-8c8b-8c7c15f0edca\";\n\nZotero.Prefs.set(pref, newSetting);\nZoteroPane.copySelectedItemsToClipboard(false);\nZotero.Prefs.set(pref, origSetting);\nreturn `QuickCopy done`;" | |
| shortcut: ⇧,⌃,C | |
| enabled: true | |
| menu: Quick copy BibTeX entry | |
| name: Quick copy BibTeX entry | |
| showInMenu: | |
| item: true | |
| collection: false | |
| tools: false | |
| reader: false | |
| readerAnnotation: false | |
| 1710769848631-0F2mzqBj: | |
| event: 0 | |
| operation: 4 | |
| data: "/**\n * QuickCopy items\n * @author Zotero Community\n * @usage Copy selected items to the clipboard\n * @link https://github.com/windingwind/zotero-actions-tags/discussions/165\n * @see https://github.com/windingwind/zotero-actions-tags/discussions/165\n */\nif (items?.length === 0 || item) {\n\treturn;\n}\n\nconst Zotero = require(\"Zotero\");\nconst ZoteroPane = require(\"ZoteroPane\");\n\nconst pref = 'export.quickCopy.setting';\nconst origSetting = Zotero.Prefs.get(pref);\n\n/**\n * Replace this line with your own setting\n * Steps:\n * 1. Change Settings -> Export -> Quick Copy -> Item Format\n * 2. Open Settings -> Advanced -> Config Editor\n * 3. Search for `export.quickCopy.setting`.\n * 4. Paste the value and replace the string `bibliography/html=http://www.zotero.org/styles/acm-siggraph` below.\n * 5. Reset Settings -> Export -> Quick Copy -> Item Format (If necessary)\n */\nconst newSetting = \"export=f895aa0d-f28e-47fe-b247-2ea77c6ed583\";\n\nZotero.Prefs.set(pref, newSetting);\nZoteroPane.copySelectedItemsToClipboard(false);\nZotero.Prefs.set(pref, origSetting);\nreturn `QuickCopy done`;" | |
| shortcut: ⇧,⌃,L | |
| enabled: true | |
| menu: Quick copy BibLaTeX entry | |
| name: Quick copy BibLaTeX entry | |
| showInMenu: | |
| item: true | |
| collection: false | |
| tools: false | |
| reader: false | |
| readerAnnotation: false | |
| 1708208087533-0F2mzqBj: | |
| event: 0 | |
| operation: 4 | |
| data: "/**\n * QuickCopy items\n * @author Zotero Community\n * @usage Copy selected items to the clipboard\n * @link https://github.com/windingwind/zotero-actions-tags/discussions/165\n * @see https://github.com/windingwind/zotero-actions-tags/discussions/165\n */\nif (items?.length === 0 || item) {\n\treturn;\n}\n\nconst Zotero = require(\"Zotero\");\nconst ZoteroPane = require(\"ZoteroPane\");\n\nconst pref = 'export.quickCopy.setting';\nconst origSetting = Zotero.Prefs.get(pref);\n\n/**\n * Replace this line with your own setting\n * Steps:\n * 1. Change Settings -> Export -> Quick Copy -> Item Format\n * 2. Open Settings -> Advanced -> Config Editor\n * 3. Search for `export.quickCopy.setting`.\n * 4. Paste the value and replace the string `bibliography/html=http://www.zotero.org/styles/acm-siggraph` below.\n * 5. Reset Settings -> Export -> Quick Copy -> Item Format (If necessary)\n */\nconst newSetting = \"export=a515a220-6fef-45ea-9842-8025dfebcc8f\";\n\nZotero.Prefs.set(pref, newSetting);\nZoteroPane.copySelectedItemsToClipboard(false);\nZotero.Prefs.set(pref, origSetting);\nreturn `QuickCopy done`;" | |
| shortcut: '' | |
| enabled: true | |
| menu: Quick copy BibLaTeX citation | |
| name: Quick copy BibLaTeX citation | |
| showInMenu: | |
| item: true | |
| collection: false | |
| tools: false | |
| reader: false | |
| readerAnnotation: false | |
| 1707921394782-0F2mzqBj: | |
| event: 0 | |
| operation: 4 | |
| data: > | |
| // Paste tags | |
| // Codes from Zutilo. | |
| // 从剪贴板获取tags | |
| var clipboardText = | |
| Zotero.Utilities.Internal.getClipboard("text/plain").trim() | |
| if (!clipboardText) { | |
| return false; | |
| } | |
| // 拆分 | |
| var tagArray = clipboardText.split(/\r\n?|\n/) | |
| tagArray = tagArray.map(function (val) { return val.trim() }); | |
| tagArray = tagArray.filter(Boolean) | |
| // 添加tags | |
| var items = Zotero.getActiveZoteroPane().getSelectedItems(); | |
| for (let item of items) { | |
| if (item.isRegularItem() && !(item instanceof Zotero.Collection)) { | |
| for (let tag of tagArray) { | |
| item.addTag(tag) | |
| } | |
| item.saveTx() | |
| } | |
| } | |
| return "Tags pasted." | |
| shortcut: ⇧,⌃,v | |
| enabled: true | |
| menu: Paste tags | |
| name: Paste tags | |
| showInMenu: | |
| item: false | |
| collection: false | |
| tools: false | |
| reader: false | |
| readerAnnotation: false | |
| 1708811994533-0F2mzqBj: | |
| event: 2 | |
| operation: 1 | |
| data: opened | |
| shortcut: '' | |
| enabled: false | |
| menu: '' | |
| name: Opened | |
| showInMenu: | |
| item: false | |
| collection: false | |
| tools: false | |
| reader: false | |
| readerAnnotation: false | |
| 1708357331797-0F2mzqBj: | |
| event: 0 | |
| operation: 4 | |
| data: "if(item) return;\n\nconst window = require(\"window\");\nconst Zotero_Tabs = require(\"Zotero_Tabs\");\n\nasync function getPDFAttachmentPath(item) {\n if (item.isAttachment() && item.attachmentContentType === 'application/pdf') {\n return await item.getFilePathAsync();\n }\n else if (item.isRegularItem() && !item.isAttachment()) {\n let attachments = await item.getAttachments();\n for (let attachmentID of attachments) {\n let attachment = await Zotero.Items.getAsync(attachmentID);\n if (attachment.attachmentContentType === 'application/pdf') {\n return await attachment.getFilePathAsync();\n }\n }\n }\n return null;\n}\n\nasync function openPDF(item) {\n let filePath = await getPDFAttachmentPath(item);\n if (!filePath) {\n Zotero.alert(window, \"Failed to open.\", \"PDF attachment not found.\");\n return \"PDF attachment not found.\";\n }\n\tlet args = [filePath, '-a', \"/Applications/Skim.app\"];\n\tlet\tapplicationPath = '/usr/bin/open';\n\tZotero.Utilities.Internal.exec(applicationPath, args);\n}\n\nif (Zotero_Tabs._selectedID !== 'zotero-pane') {\n\tlet item = Zotero.Items.get(Zotero.Reader.getByTabID(Zotero_Tabs._selectedID).itemID);\n\treturn await openPDF(item);\n}\n\nreturn await openPDF(items[0]);" | |
| shortcut: ⌘,i | |
| enabled: true | |
| menu: Open with Skim | |
| name: Open with Skim | |
| showInMenu: | |
| item: false | |
| collection: false | |
| tools: false | |
| reader: false | |
| readerAnnotation: false | |
| 1708293083187-0F2mzqBj: | |
| event: 0 | |
| operation: 4 | |
| data: "if(item) return;\n\nconst window = require(\"window\");\nconst Zotero_Tabs = require(\"Zotero_Tabs\");\n\nasync function getPDFAttachmentPath(item) {\n if (item.isAttachment() && item.attachmentContentType === 'application/pdf') {\n return await item.getFilePathAsync();\n }\n else if (item.isRegularItem() && !item.isAttachment()) {\n let attachments = await item.getAttachments();\n for (let attachmentID of attachments) {\n let attachment = await Zotero.Items.getAsync(attachmentID);\n if (attachment.attachmentContentType === 'application/pdf') {\n return await attachment.getFilePathAsync();\n }\n }\n }\n return null;\n}\n\nasync function openPDF(item) {\n let filePath = await getPDFAttachmentPath(item);\n if (!filePath) {\n Zotero.alert(window, \"Failed to open.\", \"PDF attachment not found.\");\n return \"PDF attachment not found.\";\n }\n\tlet args = [filePath, '-a', \"/Applications/Adobe\\ Acrobat\\ Reader.app\"];\n\tlet\tapplicationPath = '/usr/bin/open';\n\tZotero.Utilities.Internal.exec(applicationPath, args);\n}\n\nif (Zotero_Tabs._selectedID !== 'zotero-pane') {\n\tlet item = Zotero.Items.get(Zotero.Reader.getByTabID(Zotero_Tabs._selectedID).itemID);\n\treturn await openPDF(item);\n}\n\nreturn await openPDF(items[0]);" | |
| shortcut: ⌘,b | |
| enabled: true | |
| menu: Open with Adobe Reader | |
| name: Open with Adobe Reader | |
| showInMenu: | |
| item: false | |
| collection: false | |
| tools: false | |
| reader: false | |
| readerAnnotation: false | |
| 1707921136649-0F2mzqBj: | |
| event: 0 | |
| operation: 4 | |
| data: |- | |
| // Copy tags. | |
| // Codes from Zutilo. | |
| // 获取条目tags,并存入剪贴板 | |
| var tagsArray = []; | |
| var items = Zotero.getActiveZoteroPane().getSelectedItems(); | |
| for (let item of items) { | |
| if (item.isRegularItem() && !(item instanceof Zotero.Collection)) { | |
| var tempTags = item.getTags(); | |
| var arrayStr = ''; | |
| for (var j = 0; j < tempTags.length; j++) { | |
| arrayStr = '\n' + tagsArray.join('\n') + '\n'; | |
| let tag | |
| tag = tempTags[j].tag | |
| if (arrayStr.indexOf('\n' + tag + '\n') == -1) { | |
| tagsArray.push(tag); | |
| } | |
| } | |
| } | |
| } | |
| var clipboardText = tagsArray.join('\r\n'); | |
| Zotero.Utilities.Internal.copyTextToClipboard(clipboardText); | |
| return "Tags copied." | |
| shortcut: ⇧,⌃,T | |
| enabled: true | |
| menu: Copy tags | |
| name: Copy tags | |
| showInMenu: | |
| item: false | |
| collection: false | |
| tools: false | |
| reader: false | |
| readerAnnotation: false | |
| 1725023841803-0F2mzqBj: | |
| event: 0 | |
| operation: 4 | |
| data: |- | |
| const Zotero = require("Zotero"); | |
| const window = require("window"); | |
| (async function () { | |
| const startTime = new Date(); | |
| // Prevent duplicate execution | |
| if (window.batchTagRunning) return; | |
| window.batchTagRunning = true; | |
| // Custom capitalization dictionary | |
| const customCapitalization = { | |
| 'nist': 'NIST', | |
| 'nerc': 'NERC', | |
| // Add more custom capitalizations as needed | |
| }; | |
| // Convert a string to title case based on specified rules | |
| function toTitleCase(str, title, currentIndex, totalCount) { | |
| const lowerCaseWords = ["a", "an", "and", "as", "at", "but", "by", "for", "if", "nor", "of", "on", "or", "so", "the", "to", "up", "yet"]; | |
| const separators = ['-', ':', '–', '/', '"', '“', '”']; | |
| let words = str.split(' '); | |
| for (let i = 0; i < words.length; i++) { | |
| let word = words[i]; | |
| const lowerWord = word.toLowerCase(); | |
| // Handle custom capitalization | |
| if (customCapitalization[lowerWord]) { | |
| words[i] = customCapitalization[lowerWord]; | |
| continue; | |
| } | |
| // Capitalize the first and last word, words following a special character, and major words | |
| if (i === 0 || i === words.length - 1 || !lowerCaseWords.includes(lowerWord) || isFollowingSpecialChar(words, i, separators) || word.length >= 4) { | |
| words[i] = capitalizeHyphenatedAndSlashed(word); | |
| } else { | |
| words[i] = lowerWord; | |
| } | |
| } | |
| // Ensure words following quotation marks are capitalized | |
| for (let i = 0; i < words.length; i++) { | |
| if (words[i].startsWith('"') && words[i].length > 1) { | |
| words[i] = '"' + capitalizeFirstLetter(words[i].slice(1)); | |
| } | |
| } | |
| // Handle text within parentheses | |
| return words.join(' ').replace(/\(([^)]+)\)/g, function (match, p1) { | |
| const lowerP1 = p1.toLowerCase(); | |
| if (customCapitalization[lowerP1]) { | |
| return `(${customCapitalization[lowerP1]})`; | |
| } | |
| if (promptedValues.has(lowerP1)) { | |
| return `(${promptedValues.get(lowerP1)})`; | |
| } | |
| if (p1 !== p1.toUpperCase()) { | |
| const userResponse = window.confirm(`Do you want to convert "${p1}" to uppercase in the title "${title}"? Your response will be applied to all future occurrences of "${p1}". (Prompt ${currentIndex} of ${totalCount})`); | |
| const userResponseValue = userResponse ? p1.toUpperCase() : p1; | |
| promptedValues.set(lowerP1, userResponseValue); | |
| return `(${userResponseValue})`; | |
| } | |
| return match; | |
| }); | |
| } | |
| // Capitalize both parts of a hyphenated or slashed word | |
| function capitalizeHyphenatedAndSlashed(word) { | |
| const separators = ['-', '/']; | |
| return word.split(new RegExp(`(${separators.join('|')})`)).map(part => capitalizeFirstLetter(part)).join(''); | |
| } | |
| // Capitalize the first letter of a word | |
| function capitalizeFirstLetter(word) { | |
| return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(); | |
| } | |
| // Check if the word follows a special character (-, :, –, /, ") | |
| function isFollowingSpecialChar(arr, index, separators) { | |
| if (index > 0) { | |
| const prevWord = arr[index - 1]; | |
| return separators.some(separator => prevWord.endsWith(separator)); | |
| } | |
| return false; | |
| } | |
| // Convert a string to sentence case | |
| function toSentenceCase(str) { | |
| return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase().replace(/(\.\s*\w)/g, function (c) { | |
| return c.toUpperCase(); | |
| }); | |
| } | |
| // Convert a string to upper case | |
| function toUpperCase(str) { | |
| return str.toUpperCase(); | |
| } | |
| // Convert a string to lower case | |
| function toLowerCase(str) { | |
| return str.toLowerCase(); | |
| } | |
| // Escape special characters for use in regular expressions | |
| function escapeRegExp(string) { | |
| return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); | |
| } | |
| // Get items to edit based on user selection | |
| async function getItemsToEdit(triggerType, item, items) { | |
| const zoteroPane = Zotero.getActiveZoteroPane(); | |
| if (triggerType === 'menu') { | |
| if (items && items.length > 0) { | |
| return items; | |
| } else if (item) { | |
| return [item]; | |
| } else { | |
| window.alert("No items selected."); | |
| return null; | |
| } | |
| } else { | |
| let selectedItems = zoteroPane.getSelectedItems(); | |
| if (!selectedItems.length) { | |
| window.alert("No items selected."); | |
| return null; | |
| } | |
| return selectedItems; | |
| } | |
| } | |
| // Get all unique tags from items | |
| function getAllTags(items) { | |
| const tagSet = new Set(); | |
| for (const item of items) { | |
| const tags = item.getTags(); | |
| for (const tag of tags) { | |
| tagSet.add(tag.tag); | |
| } | |
| } | |
| return Array.from(tagSet); | |
| } | |
| // Search for tags using the provided search term | |
| function searchTags(allTags, searchTerm) { | |
| let regex; | |
| if (searchTerm.includes('*')) { | |
| regex = new RegExp(searchTerm.replace(/\*/g, '.*'), 'i'); // Replace all occurrences of '*' with '.*' | |
| } else { | |
| regex = new RegExp(escapeRegExp(searchTerm), 'i'); | |
| } | |
| return allTags.filter(tag => regex.test(tag)); | |
| } | |
| // Prompt user to select tags from search results | |
| function selectTagsFromSearchResults(tags) { | |
| if (tags.length === 0) { | |
| window.alert("No matching tags found."); | |
| return null; | |
| } | |
| const choices = tags.map((tag, index) => `${index + 1}. ${tag}`).join("\n"); | |
| const choice = window.prompt(`Select tags by numbers separated by commas or enter a new search term:\n\n${choices}`); | |
| if (choice === null) { | |
| // window.alert("Operation canceled."); | |
| return null; | |
| } | |
| const selectedIndices = choice.split(',').map(str => parseInt(str.trim(), 10) - 1); | |
| const selectedTags = selectedIndices.filter(index => !isNaN(index) && index >= 0 && index < tags.length).map(index => tags[index]); | |
| if (selectedTags.length === 0) { | |
| window.alert("No valid tags selected."); | |
| return null; | |
| } | |
| return selectedTags; | |
| } | |
| // Perform tag operations (add, remove, replace, split, combine, prefix, suffix) | |
| async function performTagOperation(operation, tags, items, newTags = [], delimiter = null) { | |
| const promises = items.map(async item => { | |
| try { | |
| if (operation === 'add') { | |
| item.addTag(tags[0]); | |
| } else if (operation === 'remove') { | |
| for (const tag of tags) { | |
| item.removeTag(tag); | |
| } | |
| } else if (operation === 'removeAll') { | |
| item.setTags([]); | |
| } else if (operation === 'replace') { | |
| for (const tag of tags) { | |
| item.removeTag(tag); | |
| } | |
| item.addTag(newTags[0]); | |
| } else if (operation === 'split') { | |
| for (const tag of tags) { | |
| const itemTags = item.getTags().map(t => t.tag); | |
| if (itemTags.includes(tag)) { | |
| item.removeTag(tag); | |
| const splitTags = tag.split(delimiter).map(t => t.trim()); | |
| for (const newTag of splitTags) { | |
| item.addTag(newTag); | |
| } | |
| } | |
| } | |
| } else if (operation === 'combine') { | |
| for (const tag of tags) { | |
| item.removeTag(tag); | |
| } | |
| for (const newTag of newTags) { | |
| item.addTag(newTag); | |
| } | |
| } else if (operation === 'prefix' || operation === 'suffix') { | |
| for (const tag of tags) { | |
| item.removeTag(tag); | |
| } | |
| for (const newTag of newTags) { | |
| item.addTag(newTag); | |
| } | |
| } | |
| await item.saveTx(); | |
| } catch (error) { | |
| Zotero.logError(`Error during ${operation} tag operation on item ${item.id}: ${error.message}`); | |
| } | |
| }); | |
| await Promise.all(promises); | |
| window.alert(`Tag operation "${operation}" completed on ${items.length} item(s).`); | |
| } | |
| // Apply case conversion to tags | |
| async function performTagCaseOperation(caseFunction, items) { | |
| const allTags = getAllTags(items); | |
| const tagMap = new Map(); | |
| for (let tag of allTags) { | |
| const newTag = caseFunction(tag, allTags.indexOf(tag) + 1, allTags.length); | |
| tagMap.set(tag, newTag); | |
| } | |
| const promises = items.map(async item => { | |
| const tags = item.getTags(); | |
| const newTags = tags.map(t => { | |
| return { tag: tagMap.get(t.tag) || t.tag }; | |
| }); | |
| item.setTags(newTags); | |
| await item.saveTx(); | |
| }); | |
| await Promise.all(promises); | |
| window.alert(`Tags have been updated for ${items.length} item(s).`); | |
| } | |
| // Log the elapsed time for a given operation | |
| function logTime(label, time) { | |
| try { | |
| Zotero.logError(`${label}: ${(time / 1000).toFixed(2)} seconds`); | |
| } catch (error) { | |
| Zotero.logError(`Failed to log time for ${label}: ${error.message}`); | |
| } | |
| } | |
| // Get valid input from the user based on predefined options | |
| async function getValidInput(promptMessage, validOptions) { | |
| while (true) { | |
| const userInput = window.prompt(promptMessage); | |
| const sanitizedInput = userInput ? userInput.trim() : null; | |
| if (sanitizedInput === null) { | |
| // window.alert("Operation canceled."); | |
| return null; | |
| } | |
| if (validOptions.includes(sanitizedInput)) { | |
| return sanitizedInput; | |
| } else { | |
| window.alert(`Invalid option: "${sanitizedInput}". Please enter one of the following: ${validOptions.join(', ')}.`); | |
| } | |
| } | |
| } | |
| // Select a base tag from search results | |
| async function selectBaseTag(allTags) { | |
| let baseTagChoices = searchTags(allTags, window.prompt("Enter a search term for the base tag:")); | |
| baseTagChoices = baseTagChoices.map((tag, index) => `${index + 1}. ${tag}`).join("\n"); | |
| const baseTagChoice = window.prompt(`Select a base tag by number or enter a new search term:\n\n${baseTagChoices}`); | |
| if (baseTagChoice === null) { | |
| window.alert("Operation canceled."); | |
| return null; | |
| } | |
| const baseTagIndex = parseInt(baseTagChoice.trim(), 10) - 1; | |
| if (isNaN(baseTagIndex) || baseTagIndex < 0 || baseTagIndex >= baseTagChoices.length) { | |
| window.alert("Invalid choice."); | |
| return null; | |
| } | |
| return baseTagChoices.split("\n")[baseTagIndex].split(". ")[1]; | |
| } | |
| // Search and select tags from search results | |
| async function searchAndSelectTags(allTags) { | |
| const searchTerm = window.prompt("Enter the regex or tag to search for:"); | |
| const matchingTags = searchTags(allTags, searchTerm); | |
| if (matchingTags.length === 0) { | |
| window.alert("No matching tags found."); | |
| return null; | |
| } | |
| return matchingTags; | |
| } | |
| try { | |
| // Ensure items and item are provided | |
| if (!items && !item) { | |
| window.alert("Bulk Edit", "No item or items array provided."); | |
| window.batchTagRunning = false; | |
| return; | |
| } | |
| // Get items to edit based on user selection | |
| let itemsToEdit = await getItemsToEdit(triggerType, item, items); | |
| if (!itemsToEdit) { | |
| window.batchTagRunning = false; | |
| return; | |
| } | |
| Zotero.logError(`Total items to edit: ${itemsToEdit.length}`); | |
| // Get the action to perform from the user | |
| const action = await getValidInput( | |
| `Choose an action: | |
| 1. Add a tag | |
| 2. Remove tags (multiple options) | |
| 3. Replace tags | |
| 4. Split a tag | |
| 5. Combine tags | |
| 6. Apply case conversion to tags | |
| 7. Add prefix or suffix to tags`, | |
| ['1', '2', '3', '4', '5', '6', '7'] | |
| ); | |
| let searchTerm, matchingTags, selectedTags, newTag, delimiter; | |
| switch (action) { | |
| case '1': | |
| const tagToAdd = window.prompt("Enter the tag to add:"); | |
| if (tagToAdd) await performTagOperation('add', [tagToAdd], itemsToEdit); | |
| // else window.alert("No tag entered."); | |
| break; | |
| case '2': | |
| const removeAction = await getValidInput( | |
| `Choose a remove option: | |
| 1. Remove a single tag | |
| 2. Remove multiple tags by search | |
| 3. Remove all tags | |
| 4. Remove all tags except specified ones`, | |
| ['1', '2', '3', '4'] | |
| ); | |
| switch (removeAction) { | |
| case '1': | |
| searchTerm = window.prompt("Enter the tag to search for:"); | |
| matchingTags = searchTags(getAllTags(itemsToEdit), searchTerm); | |
| selectedTags = selectTagsFromSearchResults(matchingTags); | |
| if (selectedTags) await performTagOperation('remove', selectedTags, itemsToEdit); | |
| break; | |
| case '2': | |
| searchTerm = window.prompt("Enter the tag search term (e.g., 'temp*' to match 'temp', 'temporary', etc.):"); | |
| matchingTags = searchTags(getAllTags(itemsToEdit), searchTerm); | |
| selectedTags = selectTagsFromSearchResults(matchingTags); | |
| if (selectedTags) await performTagOperation('remove', selectedTags, itemsToEdit); | |
| break; | |
| case '3': | |
| await performTagOperation('removeAll', [], itemsToEdit); | |
| break; | |
| case '4': | |
| searchTerm = window.prompt("Enter the tag search term to keep (e.g., 'temp*' to match 'temp', 'temporary', etc.):"); | |
| matchingTags = searchTags(getAllTags(itemsToEdit), searchTerm); | |
| selectedTags = selectTagsFromSearchResults(matchingTags); | |
| if (selectedTags) { | |
| const tagsToKeep = new Set(selectedTags); | |
| await performTagOperation('remove', getAllTags(itemsToEdit).filter(tag => !tagsToKeep.has(tag)), itemsToEdit); | |
| } | |
| break; | |
| } | |
| break; | |
| case '3': | |
| searchTerm = window.prompt("Enter the tags to search for (separated by commas):"); | |
| matchingTags = searchTags(getAllTags(itemsToEdit), searchTerm); | |
| selectedTags = selectTagsFromSearchResults(matchingTags); | |
| if (selectedTags) { | |
| newTag = window.prompt("Enter the new tag:"); | |
| if (newTag) await performTagOperation('replace', selectedTags, itemsToEdit, [newTag]); | |
| else window.alert("No new tag entered."); | |
| } | |
| break; | |
| case '4': | |
| const splitAction = await getValidInput( | |
| `Choose a split option: | |
| 1. Split a single tag | |
| 2. Split all tags by a specified delimiter`, | |
| ['1', '2'] | |
| ); | |
| if (splitAction === '1') { | |
| searchTerm = window.prompt("Enter the tag to search for:"); | |
| matchingTags = searchTags(getAllTags(itemsToEdit), searchTerm); | |
| selectedTags = selectTagsFromSearchResults(matchingTags); | |
| if (selectedTags) { | |
| delimiter = window.prompt("Enter the delimiter to split the tag:"); | |
| if (delimiter) await performTagOperation('split', selectedTags, itemsToEdit, null, delimiter); | |
| else window.alert("No delimiter entered."); | |
| } | |
| } else if (splitAction === '2') { | |
| delimiter = window.prompt("Enter the delimiter to split all tags:"); | |
| if (delimiter) await performTagOperation('split', getAllTags(itemsToEdit), itemsToEdit, null, delimiter); | |
| else window.alert("No delimiter entered."); | |
| } | |
| break; | |
| case '5': | |
| const allTags = getAllTags(itemsToEdit); | |
| const baseTag = await selectBaseTag(allTags); | |
| if (baseTag) { | |
| delimiter = window.prompt("Enter the separator to join the tags (optional):"); | |
| const tagToCombineRegex = window.prompt("Enter the regex or tag to combine with:"); | |
| if (tagToCombineRegex !== null) { | |
| const tagsToCombine = searchTags(allTags, tagToCombineRegex); | |
| if (tagsToCombine.length > 0) { | |
| const newTags = tagsToCombine.map(tag => `${baseTag}${delimiter || ''}${tag}`); | |
| await performTagOperation('combine', tagsToCombine.concat(baseTag), itemsToEdit, newTags); | |
| } else { | |
| window.alert(`No tags matching the pattern "${tagToCombineRegex}" found.`); | |
| } | |
| } else { | |
| window.alert("Invalid input for regex."); | |
| } | |
| } | |
| break; | |
| case '6': | |
| const caseOption = await getValidInput( | |
| "Enter '1' for Title Case, '2' for Sentence Case, '3' for Upper Case, '4' for Lower Case:", | |
| ['1', '2', '3', '4'] | |
| ); | |
| let caseFunction; | |
| switch (caseOption) { | |
| case '1': | |
| caseFunction = (str, currentIndex, totalCount) => toTitleCase(str, str, currentIndex, totalCount); | |
| break; | |
| case '2': | |
| caseFunction = toSentenceCase; | |
| break; | |
| case '3': | |
| caseFunction = toUpperCase; | |
| break; | |
| case '4': | |
| caseFunction = toLowerCase; | |
| break; | |
| } | |
| await performTagCaseOperation(caseFunction, itemsToEdit); | |
| break; | |
| case '7': | |
| const prefixOrSuffix = await getValidInput( | |
| "Enter '1' to add a prefix or '2' to add a suffix:", | |
| ['1', '2'] | |
| ); | |
| newTag = window.prompt(`Enter the ${prefixOrSuffix === '1' ? 'prefix' : 'suffix'} to add:`); | |
| if (!newTag) { | |
| window.alert("No prefix or suffix entered."); | |
| break; | |
| } | |
| matchingTags = await searchAndSelectTags(getAllTags(itemsToEdit)); | |
| if (matchingTags) { | |
| const newTags = matchingTags.map(tag => | |
| prefixOrSuffix === '1' ? `${newTag}${tag}` : `${tag}${newTag}` | |
| ); | |
| await performTagOperation(prefixOrSuffix === '1' ? 'prefix' : 'suffix', matchingTags, itemsToEdit, newTags); | |
| } else { | |
| window.alert("No tags matching the pattern found."); | |
| } | |
| break; | |
| default: | |
| // window.alert("Invalid action."); | |
| break; | |
| } | |
| } catch (error) { | |
| Zotero.logError(`Error in batch tag script: ${error.message}`); | |
| window.alert(`An error occurred: ${error.message}`); | |
| } finally { | |
| window.batchTagRunning = false; | |
| const endTime = new Date(); | |
| logTime("Total time", endTime - startTime); | |
| } | |
| })(); | |
| shortcut: ⌘,t | |
| enabled: true | |
| menu: Batch Tag Operations | |
| name: Batch Tag Operations | |
| showInMenu: | |
| item: true | |
| collection: true | |
| tools: true | |
| reader: false | |
| readerAnnotation: false | |
| default0: | |
| event: 1 | |
| operation: 1 | |
| data: /unread | |
| shortcut: '' | |
| enabled: false | |
| menu: '' | |
| name: Add Unread When Create Item | |
| showInMenu: | |
| item: true | |
| collection: true | |
| tools: true | |
| reader: true | |
| readerAnnotation: true |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Script for https://github.com/windingwind/zotero-actions-tags
Provides functions for Zotero 7 equivalent to those in Zotero QuickLook, Zutilo, and more.