UNPKG

biblatex-csl-converter

Version:

a set of converters: biblatex => json, CSL => json, json => biblatex, json => CSL

422 lines (369 loc) 12.6 kB
// Class to do a simple check for level 0 and 1 while waiting for a compatible // edtf.js version and figuring out if the license is OK. // It has an interface that is similar to the part of edtf.js we use so that we // can quickly switch back. // Notice: this allows open ended date ranges and it uses 1-12 rather than 0-11 for months. type SimpleDateArray = Array<string | number> type DateArray = readonly (string | number | SimpleDateArray)[] interface EDTFOutputObject { type: string valid: boolean values: DateArray cleanedString: string uncertain: boolean approximate: boolean } class SimpleEDTFParser { string: string type: string valid: boolean values: SimpleDateArray uncertain: boolean approximate: boolean parts: SimpleEDTFParser[] constructor(string: unknown) { if (!(typeof string === "string")) { console.warn(`Wrong format for EDTFParser`, string) string = "" } this.string = string as string this.type = "None" // default this.valid = true // default this.values = [] this.uncertain = false this.approximate = false this.parts = [] } init(): EDTFOutputObject { this.checkCertainty() this.splitInterval() return { type: this.type, valid: this.valid, values: this.type === "Interval" ? this.getPartValues() : this.values, cleanedString: this.cleanString(), uncertain: this.uncertain, approximate: this.approximate, } } getPartValues(): DateArray { if (this.parts.length === 0) { const emptyPart: DateArray = [] return emptyPart } else if (this.parts.length === 1) { const datePart = this.parts[0].values return datePart } else { const datePartInterval = [ this.parts[0].values, this.parts[1].values, ] return datePartInterval } } cleanString() { let cleanedString = "" if (this.parts.length) { cleanedString = this.parts .map((datePart) => datePart.cleanString()) .join("/") } else if (this.values) { cleanedString = this.values.reduce((dateString, value, index) => { if (index === 0) { if (typeof value === "number" && value > 0) { return String(value).padStart(4, "0") } else { return String(value) } } else if (index < 3) { return `${dateString}-${String(value).padStart(2, "0")}` } else if (index === 3) { return `${dateString}T${String(value).padStart(2, "0")}` } else if (index < 6) { return `${dateString}:${String(value).padStart(2, "0")}` } else { return `${dateString}${value}` } }, "") as string } if (this.uncertain) { cleanedString += "?" } if (this.approximate) { cleanedString += "~" } return cleanedString } checkCertainty() { if (this.string.slice(-1) === "~") { this.approximate = true this.string = this.string.slice(0, -1) } if (this.string.slice(-1) === "?") { this.uncertain = true this.string = this.string.slice(0, -1) } } splitInterval() { const normalizedString = this.string.replace(/--/, "/") let parts = normalizedString.split("/") if (parts.length > 2) { this.valid = false } else if (parts.length === 2) { this.type = "Interval" let valid = false // Parse both parts const parsedParts = parts.map((part) => { let parser = new SimpleEDTFParser(part) parser.init() return parser }) // Check if the individual parts are valid if ( (parsedParts[0].valid || parsedParts[0].type === "Open") && (parsedParts[1].valid || parsedParts[1].type === "Open") ) { // Now check chronological order if neither part is open if ( parsedParts[0].type === "Open" || parsedParts[1].type === "Open" ) { this.parts = parsedParts valid = true } else { // Try to compare the dates chronologically const isChronological = this.isChronologicalInterval( parsedParts[0], parsedParts[1] ) if (isChronological) { this.parts = parsedParts valid = true } else { this.valid = false } } } else { this.valid = false } if ( parsedParts[0].type === "Open" && parsedParts[1].type === "Open" ) { // From open to open is invalid this.valid = false } else if (!valid) { this.valid = false } } else { this.splitDateParts() } } isChronologicalInterval( start: SimpleEDTFParser, end: SimpleEDTFParser ): boolean { // For simplicity, we'll compare years first if (start.values.length > 0 && end.values.length > 0) { const startYear = Number(start.values[0]) const endYear = Number(end.values[0]) if (endYear < startYear) { return false // End year before start year (clearly invalid) } if (endYear > startYear) { return true // End year after start year (clearly valid) } // Years are equal, check month if available if (start.values.length > 1 && end.values.length > 1) { const startMonth = Number(start.values[1]) const endMonth = Number(end.values[1]) if (endMonth < startMonth) { return false } if (endMonth > startMonth) { return true } // Months are equal, check day if available if (start.values.length > 2 && end.values.length > 2) { const startDay = Number(start.values[2]) const endDay = Number(end.values[2]) if (endDay < startDay) { return false } if (endDay > startDay) { return true } // Could continue with hours, minutes, seconds if needed // But for brevity, equal dates are considered valid return true } } // If we get here, either: // 1. Only years were compared and they were equal (valid interval) // 2. Years and months were compared and they were equal (valid interval) return true } // If we can't extract values to compare, default to invalid return false } splitDateParts() { if (["", ".."].includes(this.string)) { // Empty string. Invalid by itself but could be valied as part of a range this.valid = false this.values = [] this.type = "Open" return } let parts = this.string.replace(/^y/, "").split(/(?!^)-/) if (parts.length > 3) { this.valid = false return } let certain = true let year = parts[0] let yearChecker = /^-?[0-9]*u{0,4}$/ // 1994, 19uu, -234, 187u, 0, 1984?~, 1uuu, uuuu, etc. if (!yearChecker.test(year)) { this.valid = false return } if (year.slice(-1) === "u") { certain = false this.type = "Interval" let from = new SimpleEDTFParser(year.replace(/u/g, "0")) from.init() let to = new SimpleEDTFParser(year.replace(/u/g, "9")) to.init() this.parts = [from, to] if (!from.valid || !to.valid) { this.valid = false } } else { this.values = [parseInt(year)] this.type = "Date" } if (parts.length < 2) { return } // Month / Season let month = parts[1] if (!certain && month !== "uu") { // End of year uncertain but month specified. Invalid this.valid = false return } let monthChecker = /^([0-2][0-9]|[1-9])|uu$/ // uu or 1, 2, 3, ..., 01, 02, 03, ..., 11, 12 let monthInt = parseInt(month.replace("uu", "01")) if ( !monthChecker.test(month) || monthInt < 1 || (monthInt > 12 && monthInt < 21) || monthInt > 24 ) { this.valid = false return } if (month === "uu") { certain = false } if (certain) { this.values.push(monthInt) } if (parts.length < 3) { if (monthInt > 12) { this.type = "Season" } return } if (monthInt > 12) { // Season + day - invalid this.valid = false return } // Day let dayTime = parts[2].split("T"), day = dayTime[0] if (!certain && day !== "uu") { // Month uncertain but day specified. Invalid this.valid = false return } let dayChecker = /^[0-3][0-9]$|uu/ // uu or 01, 02, 03, ..., 11, 12 let dayInt = parseInt(day.replace("uu", "01")) if (!dayChecker.test(month) || dayInt < 1 || dayInt > 31) { this.valid = false return } if (day === "uu") { certain = false } if (certain) { let testDate = new Date(`${year}/${month}/${day}`) if ( testDate.getFullYear() !== parseInt(year) || testDate.getMonth() + 1 !== monthInt || testDate.getDate() !== dayInt ) { this.valid = false return } this.values.push(dayInt) } if (dayTime.length < 2) { return } // Time if (!certain) { // Day uncertain but time specified this.valid = false return } let timeParts = dayTime[1] .slice(0, 8) .split(":") .map((part) => parseInt(part)) if ( timeParts.length !== 3 || timeParts[0] < 0 || timeParts[0] > 23 || timeParts[1] < 0 || timeParts[1] > 59 || timeParts[2] < 0 || timeParts[2] > 59 ) { // Invalid time this.valid = false return } this.values = this.values.concat(timeParts) if (dayTime[1].length === 8) { // No timezone return } let timeZone = dayTime[1].slice(8) if (timeZone === "Z") { // Zulu this.values.push("Z") return } let tzChecker = RegExp("^[+-][0-1][0-9]:[0-1][0-9]$"), tzParts = timeZone.split(":").map((part) => parseInt(part)) if ( !tzChecker.test(timeZone) || tzParts[0] < -11 || tzParts[0] > 14 || tzParts[1] < 0 || tzParts[1] > 59 ) { this.valid = false return } else { this.values.push(timeZone) } return } } export function edtfParse(dateString: string): EDTFOutputObject { let parser = new SimpleEDTFParser(dateString) return parser.init() }