Skip to content

Instantly share code, notes, and snippets.

@dschaehi
Last active August 31, 2024 11:59
Show Gist options
  • Select an option

  • Save dschaehi/7dae3136ad126ff66de9c88efc511ab2 to your computer and use it in GitHub Desktop.

Select an option

Save dschaehi/7dae3136ad126ff66de9c88efc511ab2 to your computer and use it in GitHub Desktop.
Actions and Tags for Zotero
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
@dschaehi
Copy link
Copy Markdown
Author

dschaehi commented Feb 18, 2024

Script for https://github.com/windingwind/zotero-actions-tags
Provides functions for Zotero 7 equivalent to those in Zotero QuickLook, Zutilo, and more.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment