Skip to content

Commit 71b3e83

Browse files
committed
Replace edtf.js with a built-in subset parser
The supported subset -- signed years, months and days, trailing qualifiers, and closed intervals -- doesn't need a full EDTF library, so parse it directly and drop the 104 KB bundle. Invalid calendar dates ("2021-02-30") are now rejected, and datetime strings are no longer treated as EDTF.
1 parent 6187884 commit 71b3e83

8 files changed

Lines changed: 60 additions & 3742 deletions

File tree

README.md

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,6 @@
33
Zotero utility code common across various codebases such as the Zotero client,
44
Zotero translation architecture and others.
55

6-
EDTF date support (`Zotero.Date.parseEDTF()` and EDTF handling in `strToMultipart()`,
7-
`itemToCSLJSON()`, and `itemFromCSLJSON()`) requires an `EDTF` global. In script
8-
environments, load `edtf.js` alongside the other files; in Node.js:
9-
10-
```js
11-
globalThis.EDTF = require('./edtf');
12-
```
13-
14-
If it isn't loaded, those functions fall back to their previous behavior.
15-
166
Item utility functions require:
177
- Calling `Zotero.Schema.init(json)` with the JSON from `schema.json` from Zotero schema repo
188
- Calling `Zotero.Date.init(json)` with the JSON from `resource/dateFormats.json`

date.js

Lines changed: 50 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -637,9 +637,9 @@ var Utilities_Date = new function(){
637637
}
638638

639639
// A date, optionally followed by a slash and a second date, where each date is a
640-
// 4-digit year with an optional minus sign followed by month/day/time/qualifier
640+
// 4-digit year with an optional minus sign followed by month/day/qualifier
641641
// characters
642-
var _edtfShapeRE = /^-?[0-9]{4}[-0-9T:+Z~?%]*(\/-?[0-9]{4}[-0-9T:+Z~?%]*)?$/i;
642+
var _edtfShapeRE = /^-?[0-9]{4}[-0-9~?%]*(\/-?[0-9]{4}[-0-9~?%]*)?$/;
643643

644644
// Era markers, with optional periods and spaces
645645
var _bceMarker = 'B\\.?\\s?C\\.?(?:\\s?E\\.?)?';
@@ -714,6 +714,14 @@ var Utilities_Date = new function(){
714714
return str.replace(/[\u2013\u2014\u2212]/g, '-').replace(/--+/g, '-');
715715
}
716716

717+
// Days in a 1-indexed month, in the proleptic Gregorian calendar
718+
function _daysInMonth(year, month) {
719+
if (month == 2) {
720+
return (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)) ? 29 : 28;
721+
}
722+
return [4, 6, 9, 11].includes(month) ? 30 : 31;
723+
}
724+
717725
// Check whether a string gives a year in a notation strToDate() misreads, either a
718726
// negative year or an era marker. An era marker counts only if the rest of the
719727
// string is EDTF punctuation, so that spelled-out dates ("January 10 200 BCE") are
@@ -785,26 +793,14 @@ var Utilities_Date = new function(){
785793
* @param {String} str
786794
* @return {Object|false} - Object with 'begin' ({year, month, day} -- month 0-indexed,
787795
* as in strToDate()), 'end' (same, for intervals), and 'circa' (true if any part
788-
* is uncertain or approximate); false if the string isn't EDTF, uses unsupported
789-
* features, or the EDTF library isn't loaded
796+
* is uncertain or approximate); false if the string isn't EDTF or uses unsupported
797+
* features
790798
*/
791799
this.parseEDTF = function (str) {
792-
if (typeof EDTF == 'undefined' || typeof str != 'string') {
800+
if (typeof str != 'string') {
793801
return false;
794802
}
795803
str = _normalizeDashes(str.trim());
796-
// Fast path for plain ISO dates, which don't need the full EDTF parser
797-
var iso = str.match(/^([0-9]{4})(?:-(0[1-9]|1[0-2])(?:-(0[1-9]|[12][0-9]|3[01]))?)?$/);
798-
if (iso) {
799-
let begin = { year: parseInt(iso[1], 10) };
800-
if (iso[2]) {
801-
begin.month = parseInt(iso[2], 10) - 1;
802-
if (iso[3]) {
803-
begin.day = parseInt(iso[3], 10);
804-
}
805-
}
806-
return { begin };
807-
}
808804
// Convert a circa prefix to an approximate qualifier below
809805
var circa = _circaPrefixRE.test(str);
810806
if (circa) {
@@ -841,65 +837,64 @@ var Utilities_Date = new function(){
841837
if (circa && !/[~?%]$/.test(str)) {
842838
str += '~';
843839
}
844-
// Skip the parser for strings that can't be in the supported EDTF subset,
840+
// Reject strings that can't be in the supported EDTF subset,
845841
// e.g., other numeric date formats like "5/13/2021" and "13.5.2021"
846842
if (!_edtfShapeRE.test(str)) {
847843
return false;
848844
}
849-
var parsed;
850-
try {
851-
parsed = EDTF.parse(str);
852-
}
853-
catch (e) {
854-
return false;
855-
}
856845

857-
// Only plain dates at level 0-1, without unspecified digits (e.g., "196X")
858-
function convertDate(date) {
859-
if (!date || date.type != 'Date' || date.level > 1 || date.unspecified) {
846+
// A single date: a signed 4-digit year, an optional month and day, and an
847+
// optional trailing uncertain/approximate qualifier ("2004-06~", "-0429?").
848+
// A qualifier anywhere else ("2004-06~-11") is rejected, as are month codes
849+
// above 12, including seasons, and invalid calendar days.
850+
function parseDate(dateStr) {
851+
var m = dateStr.match(/^(-?[0-9]{4})(?:-([0-9]{2})(?:-([0-9]{2}))?)?([~?%])?$/);
852+
if (!m) {
860853
return false;
861854
}
862-
let converted = { year: date.values[0] };
863-
if (date.values.length > 1) {
864-
converted.month = date.values[1];
865-
}
866-
if (date.values.length > 2) {
867-
converted.day = date.values[2];
855+
let date = { year: parseInt(m[1], 10) };
856+
if (m[2]) {
857+
let month = parseInt(m[2], 10);
858+
if (month < 1 || month > 12) {
859+
return false;
860+
}
861+
date.month = month - 1;
862+
if (m[3]) {
863+
let day = parseInt(m[3], 10);
864+
if (day < 1 || day > _daysInMonth(date.year, month)) {
865+
return false;
866+
}
867+
date.day = day;
868+
}
868869
}
869-
return converted;
870+
return { date, circa: !!m[4] };
870871
}
871872

872-
let result = {};
873-
if (parsed.type == 'Date') {
874-
result.begin = convertDate(parsed);
875-
if (!result.begin) {
873+
var [beginStr, endStr] = str.split('/');
874+
var begin = parseDate(beginStr);
875+
if (!begin) {
876+
return false;
877+
}
878+
let result = { begin: begin.date };
879+
if (begin.circa) {
880+
result.circa = true;
881+
}
882+
if (endStr !== undefined) {
883+
let end = parseDate(endStr);
884+
if (!end) {
876885
return false;
877886
}
878-
if (parsed.uncertain || parsed.approximate) {
887+
result.end = end.date;
888+
if (end.circa) {
879889
result.circa = true;
880890
}
881-
}
882-
else if (parsed.type == 'Interval') {
883-
result.begin = convertDate(parsed.values[0]);
884-
result.end = convertDate(parsed.values[1]);
885-
if (!result.begin || !result.end) {
886-
return false;
887-
}
888891
// Reject reversed and degenerate intervals
889892
let toComparable = date => date.year * 10000
890893
+ (date.month !== undefined ? (date.month + 1) * 100 : 0)
891894
+ (date.day || 0);
892895
if (toComparable(result.end) <= toComparable(result.begin)) {
893896
return false;
894897
}
895-
for (let value of parsed.values) {
896-
if (value.uncertain || value.approximate) {
897-
result.circa = true;
898-
}
899-
}
900-
}
901-
else {
902-
return false;
903898
}
904899
return result;
905900
};

0 commit comments

Comments
 (0)