File renaming: add support for counting creators (#5175)
Also extends the templating engine to support inequality comparisons.
This commit is contained in:
parent
9fb961fd63
commit
730d3b1f34
4 changed files with 141 additions and 9 deletions
|
@ -2488,6 +2488,9 @@ Zotero.Attachments = new function () {
|
|||
obj[name] = (args) => {
|
||||
return common(commonCreators(name, args), args);
|
||||
};
|
||||
obj[`${name}Count`] = (args) => {
|
||||
return common(getSlicedCreatorsOfType(name, Infinity).length.toString(), args);
|
||||
};
|
||||
return obj;
|
||||
}, {});
|
||||
|
||||
|
|
|
@ -2182,14 +2182,32 @@ Zotero.Utilities.Internal = {
|
|||
return [operator, args];
|
||||
};
|
||||
|
||||
// We allow unquoted numbers in conditions, e.g. `a == 1` or `a == 1.0` but not `a == 1.0.0` or `a == 1st edition`
|
||||
const asNumber = (string) => {
|
||||
if (typeof string === 'number') {
|
||||
return string;
|
||||
}
|
||||
const number = parseFloat(string);
|
||||
if (!Number.isNaN(number) && string?.trim().match(/^[+-]?\d+(\.\d+)?$/)) {
|
||||
return number;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// evaluates a condition (e.g. `a == "b"`) into a boolean value
|
||||
const evaluateCondition = (condition) => {
|
||||
const comparators = ['==', '!='];
|
||||
const comparators = ['==', '!=', "<=", ">=", '<', '>'];
|
||||
condition = condition.trim();
|
||||
|
||||
// match[1] if left is statement, match[3] if left is literal, match[4] if left is identifier
|
||||
// match[6] if right is statement, match[8] if right is literal, match[9] if right is identifier
|
||||
// match[2] and match[7] are used to match the quotes around the literal (and then check that the other quote is the same)
|
||||
|
||||
// Regular expression breakdown for condition matching:
|
||||
// - `match[1]`: Left operand if it's a statement enclosed in `{{...}}`.
|
||||
// - `match[3]`: Left operand if it's a string literal, extracted without quotes.
|
||||
// - `match[4]`: Left operand if it's a standalone identifier (e.g., a variable) or a number
|
||||
// - `match[6]`: Right operand if it's a statement enclosed in `{{...}}`.
|
||||
// - `match[8]`: Right operand if it's a string literal, extracted without quotes.
|
||||
// - `match[9]`: Right operand if it's a standalone identifier or a number.
|
||||
// - `match[2]` and `match[7]`: Captured quotes around string literals, used to ensure matching pairs.
|
||||
// - `match[5]`: The comparator (e.g., `==`, `!=`, `<`, `>`, etc.), extracted from `comparators.join('|')`.
|
||||
const match = condition.match(new RegExp(String.raw`(?:{{(.*?)}}|(?:(['"])(.*?)\2)|([^ ]+)) *(${comparators.join('|')}) *(?:{{(.*?)}}|(?:(['"])(.*?)\7)|([^ ]+))`));
|
||||
|
||||
if (!match) {
|
||||
|
@ -2201,16 +2219,24 @@ Zotero.Utilities.Internal = {
|
|||
return !!evaluateIdentifier(condition);
|
||||
}
|
||||
|
||||
const left = match[1] ? evaluateStatement(match[1]) : match[3] ?? evaluateIdentifier(match[4]) ?? '';
|
||||
const left = match[1] ? evaluateStatement(match[1]) : match[3] ?? asNumber(match[4]) ?? evaluateIdentifier(match[4]) ?? '';
|
||||
const comparator = match[5];
|
||||
const right = match[6] ? evaluateStatement(match[6]) : match[8] ?? evaluateIdentifier(match[9]) ?? '';
|
||||
const right = match[6] ? evaluateStatement(match[6]) : match[8] ?? asNumber(match[9]) ?? evaluateIdentifier(match[9]) ?? '';
|
||||
|
||||
switch (comparator) {
|
||||
default:
|
||||
case '==':
|
||||
return left.toLowerCase() == right.toLowerCase();
|
||||
return (asNumber(left) === null || asNumber(right === null)) ? left.toLowerCase() == right.toLowerCase() : asNumber(left) == asNumber(right);
|
||||
case '!=':
|
||||
return left.toLowerCase() != right.toLowerCase();
|
||||
return (asNumber(left) === null || asNumber(right === null)) ? left.toLowerCase() != right.toLowerCase() : asNumber(left) != asNumber(right);
|
||||
case ">=":
|
||||
return (asNumber(left) === null || asNumber(right === null)) ? left.toLowerCase() >= right.toLowerCase() : asNumber(left) >= asNumber(right);
|
||||
case "<=":
|
||||
return (asNumber(left) === null || asNumber(right === null)) ? left.toLowerCase() <= right.toLowerCase() : asNumber(left) <= asNumber(right);
|
||||
case '>':
|
||||
return (asNumber(left) === null || asNumber(right === null)) ? left.toLowerCase() > right.toLowerCase() : asNumber(left) > asNumber(right);
|
||||
case '<':
|
||||
return (asNumber(left) === null || asNumber(right === null)) ? left.toLowerCase() < right.toLowerCase() : asNumber(left) < asNumber(right);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -1756,6 +1756,73 @@ describe("Zotero.Attachments", function() {
|
|||
);
|
||||
});
|
||||
|
||||
it("should be possible to count authors", function () {
|
||||
assert.equal(
|
||||
Zotero.Attachments.getFileBaseNameFromItem(itemManyAuthors, { formatString: '{{ authorsCount }}' }),
|
||||
'4'
|
||||
);
|
||||
assert.equal(
|
||||
Zotero.Attachments.getFileBaseNameFromItem(itemManyAuthors, { formatString: '{{ editorsCount }}' }),
|
||||
'3'
|
||||
);
|
||||
assert.equal(
|
||||
Zotero.Attachments.getFileBaseNameFromItem(itemManyAuthors, { formatString: '{{ creatorsCount }}' }),
|
||||
'7'
|
||||
);
|
||||
assert.equal(
|
||||
Zotero.Attachments.getFileBaseNameFromItem(itemManyAuthors, { formatString: 'test{{ if authorsCount > 4 }}{{ authorsCount prefix="-" }}{{ endif }}' }),
|
||||
'test'
|
||||
);
|
||||
assert.equal(
|
||||
Zotero.Attachments.getFileBaseNameFromItem(itemManyAuthors, { formatString: 'test{{ if authorsCount >= 4 }}{{ authorsCount prefix="-" }}{{ endif }}' }),
|
||||
'test-4'
|
||||
);
|
||||
assert.equal(
|
||||
Zotero.Attachments.getFileBaseNameFromItem(itemManyAuthors, { formatString: 'test{{ if editorsCount <= 4 }}{{ editorsCount prefix="-" }}{{ endif }}' }),
|
||||
'test-3'
|
||||
);
|
||||
});
|
||||
|
||||
it("should be possible to test number of authors using equality operator", function () {
|
||||
const template = `{{ if {{ authorsCount == "2" }} }}two{{ else }}not two{{ endif }}`;
|
||||
assert.equal(
|
||||
Zotero.Attachments.getFileBaseNameFromItem(itemManyAuthors, { formatString: template }),
|
||||
'not two'
|
||||
);
|
||||
assert.equal(
|
||||
Zotero.Attachments.getFileBaseNameFromItem(item, { formatString: template }),
|
||||
'two'
|
||||
);
|
||||
});
|
||||
|
||||
it("should be possible to test number of authors using relational operators", function () {
|
||||
const template = `{{ if {{ authorsCount > "2" }} }}
|
||||
{{ authors max="1" suffix=" et al" }}
|
||||
{{ else }}
|
||||
{{ authors join=" & " }}
|
||||
{{ endif }}`;
|
||||
assert.equal(
|
||||
Zotero.Attachments.getFileBaseNameFromItem(itemManyAuthors, { formatString: template }),
|
||||
'Author et al'
|
||||
);
|
||||
assert.equal(
|
||||
Zotero.Attachments.getFileBaseNameFromItem(item, { formatString: template }),
|
||||
'Barius & Pixelus'
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle zero in relational operators", function () {
|
||||
const template = '{{ if {{ authorsCount > 0 }} }}more than zero{{ elseif {{ authorsCount <= 0 }} }}zero{{ endif }}';
|
||||
assert.equal(
|
||||
Zotero.Attachments.getFileBaseNameFromItem(item, { formatString: template }),
|
||||
'more than zero'
|
||||
);
|
||||
assert.equal(
|
||||
Zotero.Attachments.getFileBaseNameFromItem(itemIncomplete, { formatString: template }),
|
||||
'zero'
|
||||
);
|
||||
});
|
||||
|
||||
it("should perform regex in a case-insensitive way, unless configured otherwise", function () {
|
||||
assert.equal(
|
||||
Zotero.Attachments.getFileBaseNameFromItem(item, { formatString: '{{ title match="lorem" }}' }),
|
||||
|
|
|
@ -655,6 +655,42 @@ describe("Zotero.Utilities.Internal", function () {
|
|||
assert.equal(out15, 'yes');
|
||||
});
|
||||
|
||||
it("should support relational operators", function () {
|
||||
const vars = {
|
||||
sum: ({ a, b }) => (parseInt(a) + parseInt(b)).toString(),
|
||||
v1: '1',
|
||||
v2: 'foo',
|
||||
v3: '100',
|
||||
v4: '99',
|
||||
π: '3.14',
|
||||
};
|
||||
|
||||
const template1 = `{{if v1 > π}}more than π{{elseif v1 <= π}}less or equal to π{{endif}}`;
|
||||
const out1 = Zotero.Utilities.Internal.generateHTMLFromTemplate(template1, vars);
|
||||
assert.equal(out1, 'less or equal to π');
|
||||
|
||||
const template2 = `{{if {{ sum a="2" b="3" }} > π}}more than π{{else}}less or equal to π{{endif}}`;
|
||||
const out2 = Zotero.Utilities.Internal.generateHTMLFromTemplate(template2, vars);
|
||||
assert.equal(out2, 'more than π');
|
||||
|
||||
const template3 = `{{if 3.14 >= π}}more than or equal to π{{else}}less than π{{endif}}`;
|
||||
const out3 = Zotero.Utilities.Internal.generateHTMLFromTemplate(template3, vars);
|
||||
assert.equal(out3, 'more than or equal to π');
|
||||
|
||||
const template4 = `{{if v3 > v4}}100 is more than 99{{else}}string "100" would be sorted before "99"{{endif}}`;
|
||||
const out4 = Zotero.Utilities.Internal.generateHTMLFromTemplate(template4, vars);
|
||||
assert.equal(out4, '100 is more than 99');
|
||||
|
||||
// This is undocumented and unsupported behavior, but comparing strings should work
|
||||
const template5 = `{{if "test" > v2}}"t" is after "f" in the alphabet{{else}}no{{endif}}`;
|
||||
const out5 = Zotero.Utilities.Internal.generateHTMLFromTemplate(template5, vars);
|
||||
assert.equal(out5, '"t" is after "f" in the alphabet');
|
||||
|
||||
const template6 = `{{if "bar" < v2 }}"f" is before "b" in the alphabet{{else}}no{{endif}}`;
|
||||
const out6 = Zotero.Utilities.Internal.generateHTMLFromTemplate(template6, vars);
|
||||
assert.equal(out6, '"f" is before "b" in the alphabet');
|
||||
});
|
||||
|
||||
it("should accept hyphen-case variables and attributes", function () {
|
||||
const vars = {
|
||||
fooBar: ({ isFoo }) => (isFoo === 'true' ? 'foo' : 'bar'),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue