Add more features to the file renaming functionality (#4424)

* New `attachmentTitle` field, returns the title of the current attachment (or
  the future title of the attachment being created)
* New function `match` to enable testing values with a regex.
* New function `start` to enable truncating from the beginning.
* Ignore new line characters in the template for easier editing.
* Avoid repeated characters when changing case (snake/dash)
* Increase the size of the template input field.

Closes #3252
This commit is contained in:
Tom Najdek 2024-07-27 17:39:10 +02:00 committed by Dan Stillman
parent 29f4aece24
commit 00ae8bb9b2
7 changed files with 205 additions and 107 deletions

View file

@ -43,26 +43,27 @@ Zotero_Preferences.FileRenaming = {
this._itemsView.onSelect.removeListener(this._updatePreview); this._itemsView.onSelect.removeListener(this._updatePreview);
}, },
getActiveTopLevelItem() { getActiveItem() {
const selectedItem = Zotero.getActiveZoteroPane()?.getSelectedItems()?.[0]; let selectedItem = Zotero.getActiveZoteroPane()?.getSelectedItems()?.[0];
if (selectedItem) { if (selectedItem) {
if (selectedItem.isRegularItem() && !selectedItem.parentKey) { if (selectedItem.isRegularItem() && !selectedItem.parentKey) {
return [selectedItem, this.defaultExt]; return [selectedItem, this.defaultExt, ''];
} }
if (selectedItem.isFileAttachment() && selectedItem.parentKey) { if (selectedItem.isFileAttachment()) {
const path = selectedItem.getFilePath(); let path = selectedItem.getFilePath();
const ext = Zotero.File.getExtension(Zotero.File.pathToFile(path)); let ext = Zotero.File.getExtension(Zotero.File.pathToFile(path));
return [Zotero.Items.getByLibraryAndKey(selectedItem.libraryID, selectedItem.parentKey), ext ?? this.defaultExt]; let parentItem = Zotero.Items.getByLibraryAndKey(selectedItem.libraryID, selectedItem.parentKey);
return [parentItem, ext ?? this.defaultExt, selectedItem.getField('title')];
} }
} }
return null; return null;
}, },
updatePreview() { async updatePreview() {
const [item, ext] = this.getActiveTopLevelItem() ?? [this.mockItem ?? this.makeMockItem(), this.defaultExt]; const [item, ext, attachmentTitle] = this.getActiveItem() ?? [this.mockItem ?? this.makeMockItem(), this.defaultExt, ''];
const tpl = document.getElementById('file-renaming-format-template').value; const formatString = document.getElementById('file-renaming-format-template').value;
const preview = Zotero.Attachments.getFileBaseNameFromItem(item, tpl); const preview = Zotero.Attachments.getFileBaseNameFromItem(item, { formatString, attachmentTitle });
document.getElementById('file-renaming-format-preview').innerText = `${preview}.${ext}`; document.getElementById('file-renaming-format-preview').innerText = `${preview}.${ext}`;
}, },

View file

@ -57,7 +57,7 @@
aria-labelledby="file-renaming-format-template-label" aria-labelledby="file-renaming-format-template-label"
id="file-renaming-format-template" id="file-renaming-format-template"
preference="extensions.zotero.attachmentRenameTemplate" preference="extensions.zotero.attachmentRenameTemplate"
rows="3" rows="8"
/> />
<html:label id="file-renaming-format-preview-label"> <html:label id="file-renaming-format-preview-label">
<html:h2 <html:h2

View file

@ -582,7 +582,7 @@ Zotero.Attachments = new function () {
// Rename attachment // Rename attachment
if (renameIfAllowedType && !fileBaseName && this.isRenameAllowedForType(contentType)) { if (renameIfAllowedType && !fileBaseName && this.isRenameAllowedForType(contentType)) {
let parentItem = Zotero.Items.get(parentItemID); let parentItem = Zotero.Items.get(parentItemID);
fileBaseName = this.getFileBaseNameFromItem(parentItem); fileBaseName = this.getFileBaseNameFromItem(parentItem, { attachmentTitle: title });
} }
if (fileBaseName) { if (fileBaseName) {
let ext = this._getExtensionFromURL(url, contentType); let ext = this._getExtensionFromURL(url, contentType);
@ -1769,13 +1769,12 @@ Zotero.Attachments = new function () {
* @return {Zotero.Item|false} - New Zotero.Item, or false if unsuccessful * @return {Zotero.Item|false} - New Zotero.Item, or false if unsuccessful
*/ */
this.addFileFromURLs = async function (item, urlResolvers, options = {}) { this.addFileFromURLs = async function (item, urlResolvers, options = {}) {
var fileBaseName = this.getFileBaseNameFromItem(item);
var tmpDir; var tmpDir;
var tmpFile; var tmpFile;
var attachmentItem = false; var attachmentItem = false;
try { try {
tmpDir = (await this.createTemporaryStorageDirectory()).path; tmpDir = (await this.createTemporaryStorageDirectory()).path;
tmpFile = OS.Path.join(tmpDir, fileBaseName + '.tmp'); tmpFile = OS.Path.join(tmpDir, 'file.tmp');
let { title, mimeType, url, props } = await this.downloadFirstAvailableFile( let { title, mimeType, url, props } = await this.downloadFirstAvailableFile(
urlResolvers, urlResolvers,
tmpFile, tmpFile,
@ -1794,13 +1793,15 @@ Zotero.Attachments = new function () {
if (!this.FIND_AVAILABLE_FILE_TYPES.includes(mimeType)) { if (!this.FIND_AVAILABLE_FILE_TYPES.includes(mimeType)) {
throw new Error(`Resolved file is unsupported type ${mimeType}`); throw new Error(`Resolved file is unsupported type ${mimeType}`);
} }
let filename = fileBaseName + '.' + (Zotero.MIME.getPrimaryExtension(mimeType) || 'dat'); title = title || _getTitleFromVersion(props.articleVersion);
await IOUtils.move(tmpFile, PathUtils.join(tmpDir, filename)); let fileBaseName = this.getFileBaseNameFromItem(item, { attachmentTitle: title });
let ext = Zotero.MIME.getPrimaryExtension(mimeType) || 'dat';
let filename = await Zotero.File.rename(tmpFile, `${fileBaseName}.${ext}`);
attachmentItem = await this.createURLAttachmentFromTemporaryStorageDirectory({ attachmentItem = await this.createURLAttachmentFromTemporaryStorageDirectory({
directory: tmpDir, directory: tmpDir,
libraryID: item.libraryID, libraryID: item.libraryID,
filename, filename,
title: title || _getTitleFromVersion(props.articleVersion), title,
url, url,
contentType: mimeType, contentType: mimeType,
parentItemID: item.id parentItemID: item.id
@ -2207,10 +2208,16 @@ Zotero.Attachments = new function () {
* @param {Zotero.Item} item * @param {Zotero.Item} item
* @param {String} formatString * @param {String} formatString
*/ */
this.getFileBaseNameFromItem = function (item, formatString) { this.getFileBaseNameFromItem = function (item, options = {}) {
if (!(item instanceof Zotero.Item)) { if (!(item instanceof Zotero.Item)) {
throw new Error("'item' must be a Zotero.Item"); throw new Error("'item' must be a Zotero.Item");
} }
if (typeof options === 'string') {
Zotero.warn("Zotero.Attachments.getFileBaseNameFromItem(item, formatString) is deprecated -- use Zotero.Attachments(item, options)");
options = { formatString: options };
}
let { formatString = null, attachmentTitle = '' } = options;
if (!formatString) { if (!formatString) {
formatString = Zotero.Prefs.get('attachmentRenameTemplate'); formatString = Zotero.Prefs.get('attachmentRenameTemplate');
@ -2219,7 +2226,7 @@ Zotero.Attachments = new function () {
let chunks = []; let chunks = [];
let protectedLiterals = new Set(); let protectedLiterals = new Set();
formatString = formatString.trim(); formatString = formatString.replace(/\r?\n|\r/g, "").trim();
const getSlicedCreatorsOfType = (creatorType, slice) => { const getSlicedCreatorsOfType = (creatorType, slice) => {
let creatorTypeIDs; let creatorTypeIDs;
@ -2253,7 +2260,7 @@ Zotero.Attachments = new function () {
}; };
const common = (value, { truncate = false, prefix = '', suffix = '', replaceFrom = '', replaceTo = '', regexOpts = '', case: textCase = '' } = {}) => { const common = (value, { start = false, truncate = false, prefix = '', suffix = '', match = '', replaceFrom = '', replaceTo = '', regexOpts = 'i', case: textCase = '' } = {}) => {
if (value === '' || value === null || typeof value === 'undefined') { if (value === '' || value === null || typeof value === 'undefined') {
return ''; return '';
} }
@ -2266,6 +2273,17 @@ Zotero.Attachments = new function () {
suffix = ''; suffix = '';
} }
// match overrides all other options and returns immediately
if (match) {
try {
let matchResult = value.match(new RegExp(match, regexOpts));
return matchResult ? matchResult[0] : '';
}
catch (_e) {
return '';
}
}
if (protectedLiterals.size > 0) { if (protectedLiterals.size > 0) {
// escape protected literals in the format string with \ // escape protected literals in the format string with \
value = value.replace( value = value.replace(
@ -2274,8 +2292,12 @@ Zotero.Attachments = new function () {
); );
} }
if (start) {
value = value.substring(start);
}
if (truncate) { if (truncate) {
value = value.substr(0, truncate); value = value.substring(0, truncate);
} }
value = value.trim(); value = value.trim();
@ -2284,8 +2306,13 @@ Zotero.Attachments = new function () {
let affixed = false; let affixed = false;
if (replaceFrom) { if (replaceFrom) {
try {
value = value.replace(new RegExp(replaceFrom, regexOpts), replaceTo); value = value.replace(new RegExp(replaceFrom, regexOpts), replaceTo);
} }
catch (_e) {
// ignore
}
}
if (prefix && !value.startsWith(prefix)) { if (prefix && !value.startsWith(prefix)) {
value = prefix + value; value = prefix + value;
affixed = true; affixed = true;
@ -2313,9 +2340,11 @@ Zotero.Attachments = new function () {
value = Zotero.Utilities.capitalizeTitle(value, true); value = Zotero.Utilities.capitalizeTitle(value, true);
break; break;
case 'hyphen': case 'hyphen':
value = value.replace(/\s+-/g, '-').replace(/-\s+/g, '-');
value = value.toLowerCase().replace(/\s+/g, '-'); value = value.toLowerCase().replace(/\s+/g, '-');
break; break;
case 'snake': case 'snake':
value = value.replace(/\s+_/g, '_').replace(/_\s+/g, '_');
value = value.toLowerCase().replace(/\s+/g, '_'); value = value.toLowerCase().replace(/\s+/g, '_');
break; break;
case 'camel': case 'camel':
@ -2392,8 +2421,9 @@ Zotero.Attachments = new function () {
item.getField('firstCreator', true, true), args item.getField('firstCreator', true, true), args
); );
const vars = { ...fields, ...creatorFields, firstCreator, itemType, year }; const attachmentTitleFn = args => common(attachmentTitle ?? '', args);
const vars = { ...fields, ...creatorFields, attachmentTitle: attachmentTitleFn, firstCreator, itemType, year };
// Final name is generated twice. In the first pass we collect all affixed values and determine protected literals. // Final name is generated twice. In the first pass we collect all affixed values and determine protected literals.
// This is done in order to remove repeated suffixes, except if these appear in the value or the format string itself. // This is done in order to remove repeated suffixes, except if these appear in the value or the format string itself.
@ -2480,7 +2510,7 @@ Zotero.Attachments = new function () {
if (!this.isRenameAllowedForType(contentType)) { if (!this.isRenameAllowedForType(contentType)) {
return false; return false;
} }
return this.getFileBaseNameFromItem(parentItem); return this.getFileBaseNameFromItem(parentItem, { attachmentTitle: PathUtils.filename(file) });
} }

View file

@ -294,7 +294,7 @@ Zotero.RecognizeDocument = new function () {
// Rename attachment file to match new metadata // Rename attachment file to match new metadata
if (Zotero.Attachments.shouldAutoRenameFile(attachment.attachmentLinkMode == Zotero.Attachments.LINK_MODE_LINKED_FILE)) { if (Zotero.Attachments.shouldAutoRenameFile(attachment.attachmentLinkMode == Zotero.Attachments.LINK_MODE_LINKED_FILE)) {
let ext = Zotero.File.getExtension(path); let ext = Zotero.File.getExtension(path);
let fileBaseName = Zotero.Attachments.getFileBaseNameFromItem(parentItem); let fileBaseName = Zotero.Attachments.getFileBaseNameFromItem(parentItem, { attachmentTitle: originalTitle });
let newName = fileBaseName + (ext ? '.' + ext : ''); let newName = fileBaseName + (ext ? '.' + ext : '');
let result = await attachment.renameAttachmentFile(newName, false, true); let result = await attachment.renameAttachmentFile(newName, false, true);
if (result !== true) { if (result !== true) {

View file

@ -901,7 +901,7 @@ Zotero.Translate.ItemSaver.prototype = {
let fileBaseName; let fileBaseName;
if (parentItemID) { if (parentItemID) {
let parentItem = yield Zotero.Items.getAsync(parentItemID); let parentItem = yield Zotero.Items.getAsync(parentItemID);
fileBaseName = Zotero.Attachments.getFileBaseNameFromItem(parentItem); fileBaseName = Zotero.Attachments.getFileBaseNameFromItem(parentItem, { attachmentTitle: title });
} }
attachment.linkMode = "imported_url"; attachment.linkMode = "imported_url";

View file

@ -5665,7 +5665,7 @@ var ZoteroPane = new function()
let parentItemID = item.parentItemID; let parentItemID = item.parentItemID;
let parentItem = await Zotero.Items.getAsync(parentItemID); let parentItem = await Zotero.Items.getAsync(parentItemID);
var newName = Zotero.Attachments.getFileBaseNameFromItem(parentItem); var newName = Zotero.Attachments.getFileBaseNameFromItem(parentItem, { attachmentTitle: item.getField('title') });
let extRE = /\.[^\.]+$/; let extRE = /\.[^\.]+$/;
let origFilename = PathUtils.split(file).pop(); let origFilename = PathUtils.split(file).pop();

View file

@ -1364,7 +1364,8 @@ describe("Zotero.Attachments", function() {
}); });
describe("#getFileBaseNameFromItem()", function () { describe("#getFileBaseNameFromItem()", function () {
var item, itemManyAuthors, itemPatent, itemIncomplete, itemBookSection, itemSpaces, itemSuffixes, itemKeepDashes; var item, itemManyAuthors, itemPatent, itemIncomplete, itemBookSection, itemSpaces, itemSuffixes, itemKeepHyphens,
itemNoRepeatedHyphens, itemNoRepeatedUnderscores;
before(() => { before(() => {
item = createUnsavedDataObject('item', { title: 'Lorem Ipsum', itemType: 'journalArticle' }); item = createUnsavedDataObject('item', { title: 'Lorem Ipsum', itemType: 'journalArticle' });
@ -1378,6 +1379,8 @@ describe("Zotero.Attachments", function() {
item.setField('issue', '42'); item.setField('issue', '42');
item.setField('pages', '321'); item.setField('pages', '321');
itemBookSection = createUnsavedDataObject('item', { title: 'Book Section', itemType: 'bookSection' });
itemBookSection.setField('bookTitle', 'Book Title');
itemManyAuthors = createUnsavedDataObject('item', { title: 'Has Many Authors', itemType: 'book' }); itemManyAuthors = createUnsavedDataObject('item', { title: 'Has Many Authors', itemType: 'book' });
itemManyAuthors.setCreators([ itemManyAuthors.setCreators([
@ -1403,222 +1406,247 @@ describe("Zotero.Attachments", function() {
itemPatent.setField('number', 'HBK-8539b'); itemPatent.setField('number', 'HBK-8539b');
itemPatent.setField('assignee', 'Fast FooBar'); itemPatent.setField('assignee', 'Fast FooBar');
itemIncomplete = createUnsavedDataObject('item', { title: 'Incomplete', itemType: 'preprint' }); itemIncomplete = createUnsavedDataObject('item', { title: 'Incomplete', itemType: 'preprint' });
itemBookSection = createUnsavedDataObject('item', { title: 'Book Section', itemType: 'bookSection' });
itemBookSection.setField('bookTitle', 'Book Title');
itemSpaces = createUnsavedDataObject('item', { title: ' Spaces! ', itemType: 'book' }); itemSpaces = createUnsavedDataObject('item', { title: ' Spaces! ', itemType: 'book' });
itemSuffixes = createUnsavedDataObject('item', { title: '-Suffixes-', itemType: 'book' }); itemSuffixes = createUnsavedDataObject('item', { title: '-Suffixes-', itemType: 'book' });
itemSuffixes.setField('date', "1999-07-15"); itemSuffixes.setField('date', "1999-07-15");
itemKeepDashes = createUnsavedDataObject('item', { title: 'keep--dashes', itemType: 'journalArticle' }); itemKeepHyphens = createUnsavedDataObject('item', { title: 'keep--hyphens', itemType: 'journalArticle' });
itemKeepDashes.setField('publicationTitle', "keep"); itemKeepHyphens.setField('publicationTitle', "keep");
itemKeepDashes.setField('issue', 'dashes'); itemKeepHyphens.setField('issue', 'hyphens');
itemKeepDashes.setField('date', "1999-07-15"); itemKeepHyphens.setField('date', "1999-07-15");
itemNoRepeatedHyphens = createUnsavedDataObject('item', { title: 'no - repeated - hyphens', itemType: 'journalArticle' });
itemNoRepeatedHyphens.setField('publicationTitle', "no- repeated- hyphens");
itemNoRepeatedUnderscores = createUnsavedDataObject('item', { title: 'no _ repeated _ underscores', itemType: 'journalArticle' });
itemNoRepeatedUnderscores.setField('publicationTitle', "no_ repeated_ underscores");
}); });
it('should strip HTML tags from title', function () { it('should strip HTML tags from title', function () {
var htmlItem = createUnsavedDataObject('item', { title: 'Foo <i>Bar</i> Foo<br><br/><br />Bar' }); var htmlItem = createUnsavedDataObject('item', { title: 'Foo <i>Bar</i> Foo<br><br/><br />Bar' });
var str = Zotero.Attachments.getFileBaseNameFromItem(htmlItem, '{{ title }}'); var str = Zotero.Attachments.getFileBaseNameFromItem(htmlItem, { formatString: '{{ title }}' });
assert.equal(str, 'Foo Bar Foo Bar'); assert.equal(str, 'Foo Bar Foo Bar');
}); });
it('should accept basic formating options', function () { it('should accept basic formating options', function () {
assert.equal( assert.equal(
Zotero.Attachments.getFileBaseNameFromItem(item, 'FOO{{year}}BAR'), Zotero.Attachments.getFileBaseNameFromItem(item, { formatString: 'FOO{{year}}BAR' }),
'FOO1975BAR' 'FOO1975BAR'
); );
assert.equal( assert.equal(
Zotero.Attachments.getFileBaseNameFromItem(item, '{{firstCreator suffix=" - "}}{{year suffix=" - "}}{{title truncate="50" }}'), Zotero.Attachments.getFileBaseNameFromItem(item, { formatString: '{{firstCreator suffix=" - "}}{{year suffix=" - "}}{{title truncate="50" }}' }),
'Barius and Pixelus - 1975 - Lorem Ipsum' 'Barius and Pixelus - 1975 - Lorem Ipsum'
); );
assert.equal( assert.equal(
Zotero.Attachments.getFileBaseNameFromItem(item, '{{firstCreator suffix=" - " replaceFrom=" *and *" replaceTo="&"}}{{year suffix=" - " replaceFrom="(\\d{2})(\\d{2})" replaceTo="$2"}}{{title truncate="50" replaceFrom=".m" replaceTo="a"}} - {{title truncate="50" replaceFrom=".m" replaceTo="a" regexOpts="g"}}'), Zotero.Attachments.getFileBaseNameFromItem(item, { formatString: '{{firstCreator suffix=" - " replaceFrom=" *and *" replaceTo="&"}}{{year suffix=" - " replaceFrom="(\\d{2})(\\d{2})" replaceTo="$2"}}{{title truncate="50" replaceFrom=".m" replaceTo="a"}} - {{title truncate="50" replaceFrom=".m" replaceTo="a" regexOpts="g"}}' }),
'Barius&Pixelus - 75 - Lora Ipsum - Lora Ipsa' 'Barius&Pixelus - 75 - Lora Ipsum - Lora Ipsa'
); );
assert.equal( assert.equal(
Zotero.Attachments.getFileBaseNameFromItem(item, '{{year suffix="-"}}{{firstCreator truncate="10" suffix="-"}}{{title truncate="5" }}'), Zotero.Attachments.getFileBaseNameFromItem(item, { formatString: '{{year suffix="-"}}{{firstCreator truncate="10" suffix="-"}}{{title truncate="5" }}' }),
'1975-Barius and-Lorem' '1975-Barius and-Lorem'
); );
assert.equal( assert.equal(
Zotero.Attachments.getFileBaseNameFromItem(item, 'foo {{year}} bar {{year prefix="++" truncate="2" suffix="++"}}'), Zotero.Attachments.getFileBaseNameFromItem(item, { formatString: 'foo {{year}} bar {{year prefix="++" truncate="2" suffix="++"}}' }),
'foo 1975 bar ++19++' 'foo 1975 bar ++19++'
); );
assert.equal( assert.equal(
Zotero.Attachments.getFileBaseNameFromItem(itemManyAuthors, '{{firstCreator suffix=" - "}}{{year suffix=" - "}}{{title}}'), Zotero.Attachments.getFileBaseNameFromItem(itemManyAuthors, { formatString: '{{firstCreator suffix=" - "}}{{year suffix=" - "}}{{title}}' }),
'Author et al. - 2000 - Has Many Authors' 'Author et al. - 2000 - Has Many Authors'
); );
}); });
it('should trim whitespaces from a value', function () { it('should trim whitespaces from a value', function () {
assert.equal( assert.equal(
Zotero.Attachments.getFileBaseNameFromItem(itemSpaces, '{{ title }}'), Zotero.Attachments.getFileBaseNameFromItem(itemSpaces, { formatString: '{{ title }}' }),
'Spaces!' 'Spaces!'
); );
assert.equal( assert.equal(
Zotero.Attachments.getFileBaseNameFromItem(item, '{{title truncate="6"}}'), Zotero.Attachments.getFileBaseNameFromItem(item, { formatString: '{{title truncate="6"}}' }),
'Lorem' 'Lorem'
); );
assert.equal( assert.equal(
Zotero.Attachments.getFileBaseNameFromItem(item, '{{firstCreator truncate="7"}}'), Zotero.Attachments.getFileBaseNameFromItem(item, { formatString: '{{firstCreator truncate="7"}}' }),
'Barius' 'Barius'
); );
// but preserve if it's configured as a prefix or suffix // but preserve if it's configured as a prefix or suffix
assert.equal( assert.equal(
Zotero.Attachments.getFileBaseNameFromItem(item, '{{title prefix=" " suffix=" "}}'), Zotero.Attachments.getFileBaseNameFromItem(item, { formatString: '{{title prefix=" " suffix=" "}}' }),
' Lorem Ipsum ' ' Lorem Ipsum '
); );
}); });
it('should offer a range of options for composing creators', function () { it('should offer a range of options for composing creators', async function () {
assert.equal( assert.equal(
Zotero.Attachments.getFileBaseNameFromItem(item, '{{ authors max="1" }}'), Zotero.Attachments.getFileBaseNameFromItem(item, { formatString: '{{ authors max="1" }}' }),
'Barius' 'Barius'
); );
assert.equal( assert.equal(
Zotero.Attachments.getFileBaseNameFromItem(item, '{{ authors max="1" truncate="3" }}'), Zotero.Attachments.getFileBaseNameFromItem(item, { formatString: '{{ authors max="1" truncate="3" }}' }),
'Bar' 'Bar'
); );
assert.equal( assert.equal(
Zotero.Attachments.getFileBaseNameFromItem(item, '{{ authors max="5" join=" " }}'), Zotero.Attachments.getFileBaseNameFromItem(item, { formatString: '{{ authors max="5" join=" " }}' }),
'Barius Pixelus' 'Barius Pixelus'
); );
assert.equal( assert.equal(
Zotero.Attachments.getFileBaseNameFromItem(itemManyAuthors, '{{ authors max="3" join=" " }}'), Zotero.Attachments.getFileBaseNameFromItem(itemManyAuthors, { formatString: '{{ authors max="3" join=" " }}' }),
'Author Creator Person' 'Author Creator Person'
); );
assert.equal( assert.equal(
Zotero.Attachments.getFileBaseNameFromItem(itemPatent, '{{ authors }}'), Zotero.Attachments.getFileBaseNameFromItem(itemPatent, { formatString: '{{ authors }}' }),
'AcmeCorp' 'AcmeCorp'
); );
assert.equal( assert.equal(
Zotero.Attachments.getFileBaseNameFromItem(itemManyAuthors, '{{ authors max="2" name="family" initialize="family" join=" " initialize-with="" }}'), Zotero.Attachments.getFileBaseNameFromItem(itemManyAuthors, { formatString: '{{ authors max="2" name="family" initialize="family" join=" " initialize-with="" }}' }),
'A C' 'A C'
); );
assert.equal( assert.equal(
Zotero.Attachments.getFileBaseNameFromItem(itemPatent, '{{ authors max="2" name="family" initialize="family" initialize-with="" }}'), Zotero.Attachments.getFileBaseNameFromItem(itemPatent, { formatString: '{{ authors max="2" name="family" initialize="family" initialize-with="" }}' }),
'A' 'A'
); );
assert.equal( assert.equal(
Zotero.Attachments.getFileBaseNameFromItem(item, '{{ authors max="1" name="full" initialize="full" name-part-separator="" initialize-with="" }}'), Zotero.Attachments.getFileBaseNameFromItem(item, { formatString: '{{ authors max="1" name="full" initialize="full" name-part-separator="" initialize-with="" }}' }),
'FB' 'FB'
); );
assert.equal( assert.equal(
Zotero.Attachments.getFileBaseNameFromItem(itemManyAuthors, '{{ authors max="3" name="full" initialize="full" name-part-separator="" join=" " initialize-with="" }}'), Zotero.Attachments.getFileBaseNameFromItem(itemManyAuthors, { formatString: '{{ authors max="3" name="full" initialize="full" name-part-separator="" join=" " initialize-with="" }}' }),
'FA SC TP' 'FA SC TP'
); );
assert.equal( assert.equal(
Zotero.Attachments.getFileBaseNameFromItem(item, '{{ authors max="1" name="family-given" initialize="given" name-part-separator="" initialize-with="" }}'), Zotero.Attachments.getFileBaseNameFromItem(item, { formatString: '{{ authors max="1" name="family-given" initialize="given" name-part-separator="" initialize-with="" }}' }),
'BariusF' 'BariusF'
); );
assert.equal( assert.equal(
Zotero.Attachments.getFileBaseNameFromItem(itemManyAuthors, '{{ authors max="2" name="family-given" initialize="given" join=" " name-part-separator="" initialize-with="" }}'), Zotero.Attachments.getFileBaseNameFromItem(itemManyAuthors, { formatString: '{{ authors max="2" name="family-given" initialize="given" join=" " name-part-separator="" initialize-with="" }}' }),
'AuthorF CreatorS' 'AuthorF CreatorS'
); );
assert.equal( assert.equal(
Zotero.Attachments.getFileBaseNameFromItem(item, '{{ editors }}test'), Zotero.Attachments.getFileBaseNameFromItem(item, { formatString: '{{ editors }}test' }),
'test' 'test'
); );
assert.equal( assert.equal(
Zotero.Attachments.getFileBaseNameFromItem(itemManyAuthors, '{{ editors max="1" }}'), Zotero.Attachments.getFileBaseNameFromItem(itemManyAuthors, { formatString: '{{ editors max="1" }}' }),
'Editor1' 'Editor1'
); );
assert.equal( assert.equal(
Zotero.Attachments.getFileBaseNameFromItem(itemManyAuthors, '{{ editors max="5" join=" " }}'), Zotero.Attachments.getFileBaseNameFromItem(itemManyAuthors, { formatString: '{{ editors max="5" join=" " }}' }),
'Editor1 ProEditor2 SuperbEditor3' 'Editor1 ProEditor2 SuperbEditor3'
); );
assert.equal( assert.equal(
Zotero.Attachments.getFileBaseNameFromItem(itemManyAuthors, '{{ editors max="2" name="family" initialize="family" join=" " initialize-with="" }}'), Zotero.Attachments.getFileBaseNameFromItem(itemManyAuthors, { formatString: '{{ editors max="2" name="family" initialize="family" join=" " initialize-with="" }}' }),
'E P' 'E P'
); );
assert.equal( assert.equal(
Zotero.Attachments.getFileBaseNameFromItem(itemManyAuthors, '{{ editors max="1" name="full" initialize="full" name-part-separator="" initialize-with="" }}'), Zotero.Attachments.getFileBaseNameFromItem(itemManyAuthors, { formatString: '{{ editors max="1" name="full" initialize="full" name-part-separator="" initialize-with="" }}' }),
'SE' 'SE'
); );
assert.equal( assert.equal(
Zotero.Attachments.getFileBaseNameFromItem(itemManyAuthors, '{{ editors max="1" name="family-given" initialize="given" name-part-separator="" initialize-with="" }}'), Zotero.Attachments.getFileBaseNameFromItem(itemManyAuthors, { formatString: '{{ editors max="1" name="family-given" initialize="given" name-part-separator="" initialize-with="" }}' }),
'Editor1S' 'Editor1S'
); );
assert.equal( assert.equal(
Zotero.Attachments.getFileBaseNameFromItem(item, '{{ authors max="3" name="full" initialize="given" }}'), Zotero.Attachments.getFileBaseNameFromItem(item, { formatString: '{{ authors max="3" name="full" initialize="given" }}' }),
'F. Barius, B. Pixelus' 'F. Barius, B. Pixelus'
); );
assert.equal( assert.equal(
Zotero.Attachments.getFileBaseNameFromItem(item, '{{ creators case="upper" }}'), Zotero.Attachments.getFileBaseNameFromItem(item, { formatString: '{{ creators case="upper" }}' }),
'BARIUS, PIXELUS' 'BARIUS, PIXELUS'
); );
assert.equal( assert.equal(
Zotero.Attachments.getFileBaseNameFromItem(itemManyAuthors, '{{ authors max="2" }}'), Zotero.Attachments.getFileBaseNameFromItem(itemManyAuthors, { formatString: '{{ authors max="2" }}' }),
'Author, Creator' 'Author, Creator'
); );
assert.equal( assert.equal(
Zotero.Attachments.getFileBaseNameFromItem(itemManyAuthors, '{{ creators max="3" join=" " name="given" }}'), Zotero.Attachments.getFileBaseNameFromItem(itemManyAuthors, { formatString: '{{ creators max="3" join=" " name="given" }}' }),
'First Second Third' 'First Second Third'
); );
}); });
it('should accept case parameter', async function () { it('should accept case parameter', async function () {
assert.equal( assert.equal(
Zotero.Attachments.getFileBaseNameFromItem(item, '{{ publicationTitle case="upper" }}'), Zotero.Attachments.getFileBaseNameFromItem(item, { formatString: '{{ publicationTitle case="upper" }}' }),
'BEST PUBLICATIONS PLACE' 'BEST PUBLICATIONS PLACE'
); );
assert.equal( assert.equal(
Zotero.Attachments.getFileBaseNameFromItem(item, '{{ publicationTitle case="lower" }}'), Zotero.Attachments.getFileBaseNameFromItem(item, { formatString: '{{ publicationTitle case="lower" }}' }),
'best publications place' 'best publications place'
); );
assert.equal( assert.equal(
Zotero.Attachments.getFileBaseNameFromItem(item, '{{ publicationTitle case="title" }}'), Zotero.Attachments.getFileBaseNameFromItem(item, { formatString: '{{ publicationTitle case="title" }}' }),
'Best Publications Place' 'Best Publications Place'
); );
assert.equal( assert.equal(
Zotero.Attachments.getFileBaseNameFromItem(item, '{{ publicationTitle case="hyphen" }}'), Zotero.Attachments.getFileBaseNameFromItem(item, { formatString: '{{ publicationTitle case="hyphen" }}' }),
'best-publications-place' 'best-publications-place'
); );
assert.equal( assert.equal(
Zotero.Attachments.getFileBaseNameFromItem(item, '{{ publicationTitle case="camel" }}'), Zotero.Attachments.getFileBaseNameFromItem(item, { formatString: '{{ publicationTitle case="camel" }}' }),
'bestPublicationsPlace' 'bestPublicationsPlace'
); );
assert.equal( assert.equal(
Zotero.Attachments.getFileBaseNameFromItem(item, '{{ publicationTitle case="snake" }}'), Zotero.Attachments.getFileBaseNameFromItem(item, { formatString: '{{ publicationTitle case="snake" }}' }),
'best_publications_place' 'best_publications_place'
); );
}); });
it('should accept itemType or any other field', function () { it('should not create repeated characters when converting case to hyphen or snake', async function () {
assert.equal( assert.equal(
Zotero.Attachments.getFileBaseNameFromItem(item, '{{ itemType localize="true" }}'), Zotero.Attachments.getFileBaseNameFromItem(itemNoRepeatedHyphens, { formatString: '{{ title case="hyphen" }}' }),
'no-repeated-hyphens'
);
assert.equal(
Zotero.Attachments.getFileBaseNameFromItem(itemNoRepeatedHyphens, { formatString: '{{ publicationTitle case="hyphen" }}' }),
'no-repeated-hyphens'
);
assert.equal(
Zotero.Attachments.getFileBaseNameFromItem(itemNoRepeatedUnderscores, { formatString: '{{ title case="snake" }}' }),
'no_repeated_underscores'
);
assert.equal(
Zotero.Attachments.getFileBaseNameFromItem(itemNoRepeatedUnderscores, { formatString: '{{ publicationTitle case="snake" }}' }),
'no_repeated_underscores'
);
});
it('should accept itemType, attachmentTitle or any other field', function () {
assert.equal(
Zotero.Attachments.getFileBaseNameFromItem(item, { formatString: '{{ itemType localize="true" }}' }),
'Journal Article' 'Journal Article'
); );
assert.equal( assert.equal(
Zotero.Attachments.getFileBaseNameFromItem(item, '{{ publicationTitle }}'), Zotero.Attachments.getFileBaseNameFromItem(item, { formatString: '{{ publicationTitle }}' }),
'Best Publications Place' 'Best Publications Place'
); );
assert.equal( assert.equal(
Zotero.Attachments.getFileBaseNameFromItem(item, '{{ journalAbbreviation }}'), Zotero.Attachments.getFileBaseNameFromItem(item, { formatString: '{{ journalAbbreviation }}' }),
'BPP' 'BPP'
); );
assert.equal( assert.equal(
Zotero.Attachments.getFileBaseNameFromItem(itemManyAuthors, '{{ publisher }}'), Zotero.Attachments.getFileBaseNameFromItem(itemManyAuthors, { formatString: '{{ publisher }}' }),
'Awesome House' 'Awesome House'
); );
assert.equal( assert.equal(
Zotero.Attachments.getFileBaseNameFromItem(itemManyAuthors, '{{ volume }}'), Zotero.Attachments.getFileBaseNameFromItem(itemManyAuthors, { formatString: '{{ volume }}' }),
'3' '3'
); );
assert.equal( assert.equal(
Zotero.Attachments.getFileBaseNameFromItem(item, '{{ issue }}'), Zotero.Attachments.getFileBaseNameFromItem(item, { formatString: '{{ issue }}' }),
'42' '42'
); );
assert.equal( assert.equal(
Zotero.Attachments.getFileBaseNameFromItem(item, '{{ pages }}'), Zotero.Attachments.getFileBaseNameFromItem(item, { formatString: '{{ pages }}' }),
'321' '321'
); );
assert.equal( assert.equal(
Zotero.Attachments.getFileBaseNameFromItem(itemPatent, '{{ number }}'), Zotero.Attachments.getFileBaseNameFromItem(itemPatent, { formatString: '{{ number }}' }),
'HBK-8539b' 'HBK-8539b'
); );
assert.equal( assert.equal(
Zotero.Attachments.getFileBaseNameFromItem(itemPatent, '{{ assignee }}'), Zotero.Attachments.getFileBaseNameFromItem(itemPatent, { formatString: '{{ assignee }}' }),
'Fast FooBar' 'Fast FooBar'
); );
assert.equal(
Zotero.Attachments.getFileBaseNameFromItem(item, { formatString: '{{ attachmentTitle }}', attachmentTitle: 'Full Text' }),
'Full Text'
);
}); });
it("should support simple logic in template syntax", function () { it("should support simple logic in template syntax", function () {
@ -1634,68 +1662,107 @@ describe("Zotero.Attachments", function() {
); );
}); });
it("should skip missing fields", function () { it("should skip missing fields", async function () {
assert.equal( assert.equal(
Zotero.Attachments.getFileBaseNameFromItem(itemIncomplete, '{{ authors prefix = "a" suffix="-" }}{{ publicationTitle case="hyphen" suffix="-" }}{{ title }}'), Zotero.Attachments.getFileBaseNameFromItem(itemIncomplete, { formatString: '{{ authors prefix = "a" suffix="-" }}{{ publicationTitle case="hyphen" suffix="-" }}{{ title }}' }),
'Incomplete' 'Incomplete'
); );
}); });
it("should recognized base-mapped fields", function () { it("should recognized base-mapped fields", function () {
assert.equal( assert.equal(
Zotero.Attachments.getFileBaseNameFromItem(itemBookSection, '{{ bookTitle case="snake" }}'), Zotero.Attachments.getFileBaseNameFromItem(itemBookSection, { formatString: '{{ bookTitle case="snake" }}' }),
'book_title' 'book_title'
); );
assert.equal( assert.equal(
Zotero.Attachments.getFileBaseNameFromItem(itemBookSection, '{{ publicationTitle case="snake" }}'), Zotero.Attachments.getFileBaseNameFromItem(itemBookSection, { formatString: '{{ publicationTitle case="snake" }}' }),
'book_title' 'book_title'
); );
}); });
it("should trim spaces from template string", function () { it("should trim spaces and remove new lines from the template string", function () {
assert.equal( assert.equal(
Zotero.Attachments.getFileBaseNameFromItem(itemBookSection, ' {{ bookTitle case="snake" }} '), Zotero.Attachments.getFileBaseNameFromItem(itemBookSection, { formatString: ' {{ bookTitle case="snake" }}\n{{ bookTitle case="hyphen" prefix="-" }}' }),
'book_title' 'book_title-book-title'
); );
}); });
it("should suppress suffixes where they would create a repeat character", function () { it("should suppress suffixes where they would create a repeat character", function () {
assert.equal( assert.equal(
Zotero.Attachments.getFileBaseNameFromItem(item, '{{ title suffix="-" }}{{ year prefix="-" }}'), Zotero.Attachments.getFileBaseNameFromItem(item, { formatString: '{{ title suffix="-" }}{{ year prefix="-" }}' }),
'Lorem Ipsum-1975' 'Lorem Ipsum-1975'
); );
assert.equal( assert.equal(
Zotero.Attachments.getFileBaseNameFromItem(itemSuffixes, '{{ title prefix="-" suffix="-" }}{{ year }}'), Zotero.Attachments.getFileBaseNameFromItem(itemSuffixes, { formatString: '{{ title prefix="-" suffix="-" }}{{ year }}' }),
'-Suffixes-1999' '-Suffixes-1999'
); );
assert.equal( assert.equal(
Zotero.Attachments.getFileBaseNameFromItem(itemSuffixes, '{{ title suffix="-" }}{{ year prefix="-" }}'), Zotero.Attachments.getFileBaseNameFromItem(itemSuffixes, { formatString: '{{ title suffix="-" }}{{ year prefix="-" }}' }),
'-Suffixes-1999' '-Suffixes-1999'
); );
assert.equal( assert.equal(
Zotero.Attachments.getFileBaseNameFromItem(itemKeepDashes, '{{ title suffix="-" }}{{ year prefix="-" }}'), Zotero.Attachments.getFileBaseNameFromItem(itemKeepHyphens, { formatString: '{{ title suffix="-" }}{{ year prefix="-" }}' }),
'keep--dashes-1999' 'keep--hyphens-1999'
); );
// keep--dashes is a title and should be kept unchanged but "keep" and "dashes" are fields // keep--hyphens is a title and should be kept unchanged but "keep" and "hyphens" are fields
// separated by prefixes and suffixes where repeated characters should be suppressed // separated by prefixes and suffixes where repeated characters should be suppressed
assert.equal( assert.equal(
Zotero.Attachments.getFileBaseNameFromItem(itemKeepDashes, '{{ title suffix="-" }}{{ publicationTitle suffix="-" }}{{ issue prefix="-" }}'), Zotero.Attachments.getFileBaseNameFromItem(itemKeepHyphens, { formatString: '{{ title suffix="-" }}{{ publicationTitle suffix="-" }}{{ issue prefix="-" }}' }),
'keep--dashes-keep-dashes' 'keep--hyphens-keep-hyphens'
); );
// keep--dashes is provided as literal part of the template and should be kept unchanged // keep--hyphens is provided as literal part of the template and should be kept unchanged
// but "keep" and "dashes" are fields separated by prefixes and suffixes where repeated // but "keep" and "hyphens" are fields separated by prefixes and suffixes where repeated
// characters should be suppressed. Finally "keep--dashes" title is appended at the end // characters should be suppressed. Finally "keep--hyphens" title is appended at the end
// which should also be kept as is. // which should also be kept as is.
assert.equal( assert.equal(
Zotero.Attachments.getFileBaseNameFromItem(itemKeepDashes, 'keep--dashes-{{ publicationTitle prefix="-" suffix="-" }}{{ issue prefix="-" suffix="-" }}-keep--dashes-{{ publicationTitle suffix="-" }}test{{ title prefix="-" }}'), Zotero.Attachments.getFileBaseNameFromItem(itemKeepHyphens, { formatString: 'keep--hyphens-{{ publicationTitle prefix="-" suffix="-" }}{{ issue prefix="-" suffix="-" }}-keep--hyphens-{{ publicationTitle suffix="-" }}test{{ title prefix="-" }}' }),
'keep--dashes-keep-dashes-keep--dashes-keep-test-keep--dashes' 'keep--hyphens-keep-hyphens-keep--hyphens-keep-test-keep--hyphens'
); );
assert.equal( assert.equal(
Zotero.Attachments.getFileBaseNameFromItem(itemSuffixes, '{{ title prefix="/" suffix="\\" }}{{ year }}'), Zotero.Attachments.getFileBaseNameFromItem(itemSuffixes, { formatString: '{{ title prefix="/" suffix="\\" }}{{ year }}' }),
'-Suffixes-1999' '-Suffixes-1999'
); );
}); });
it("should be possible to test attachmentTitle", function () {
const template = `{{ if {{ attachmentTitle match="^(full.*|submitted.*|accepted.*)$" }} }}
{{ firstCreator suffix=" - " }}{{ year suffix=" - " }}{{ title truncate="100" }}
{{ else }}
{{ attachmentTitle replaceFrom="\\.pdf|\\.epub|\\.png" }}
{{ endif }}`;
assert.equal(
Zotero.Attachments.getFileBaseNameFromItem(item, { formatString: template, attachmentTitle: 'Full Text' }),
'Barius and Pixelus - 1975 - Lorem Ipsum'
);
assert.equal(
Zotero.Attachments.getFileBaseNameFromItem(itemBookSection, { formatString: template, attachmentTitle: 'Other Attachment.png' }),
'Other Attachment'
);
assert.equal(
Zotero.Attachments.getFileBaseNameFromItem(itemBookSection, { formatString: `{{ attachmentTitle start = "6" truncate = "4" }}`, attachmentTitle: 'Other Attachment.png' }),
'Atta'
);
});
it("should perform regex in a case-insensitive way, unless configured otherwise", function () {
assert.equal(
Zotero.Attachments.getFileBaseNameFromItem(item, { formatString: '{{ title match="lorem" }}' }),
'Lorem'
);
assert.equal(
Zotero.Attachments.getFileBaseNameFromItem(item, { formatString: '{{ title match="lorem" regexOpts="" }}' }),
'_' // template formatting results in an empty string, "_" is returned to make it a valid file name
);
assert.equal(
Zotero.Attachments.getFileBaseNameFromItem(item, { formatString: '{{ title replaceFrom="lorem" replaceTo="Foobar" }}' }),
'Foobar Ipsum'
);
assert.equal(
Zotero.Attachments.getFileBaseNameFromItem(item, { formatString: '{{ title replaceFrom="lorem" replaceTo="foobar" regexOpts="" }}' }),
'Lorem Ipsum'
);
});
it("should convert old attachmentRenameFormatString to use new attachmentRenameTemplate syntax", function () { it("should convert old attachmentRenameFormatString to use new attachmentRenameTemplate syntax", function () {
assert.equal( assert.equal(
Zotero.Prefs.convertLegacyAttachmentRenameFormatString('{%c - }{%y - }{%t{50}}'), Zotero.Prefs.convertLegacyAttachmentRenameFormatString('{%c - }{%y - }{%t{50}}'),