"use strict";
define(["common/XMLUtilities"], (XMLUtilities) => {
/**
* @namespace EMLUtilities
* @description A generic utility object that contains functions used
* throughout MetacatUI to perform useful functions related to EML, but not
* used to store or manipulate any state about the application.
* @type {object}
* @since 2.34.0
*/
const EMLUtilities = /** @lends EMLUtilities.prototype */ {
/**
* Validate a year with four digits or a YYYY-MM-DD calendar date.
* @param {string} dateString Date text to check
* @returns {boolean} True if the format and calendar date are valid
* @since 2.41.0
*/
isDateFormatValid(dateString) {
if (/^\d{4}$/.test(dateString)) return true;
if (!/^\d{4}-\d{2}-\d{2}$/.test(dateString)) return false;
const [year, month, day] = dateString.split("-").map(Number);
if (month < 1 || month > 12 || day < 1) return false;
if (month === 2) {
const leapYear =
year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
return day <= (leapYear ? 29 : 28);
}
return day <= ([4, 6, 9, 11].includes(month) ? 30 : 31);
},
/**
* Parse an EML xs:time value without rounding fractional seconds or
* assuming a time zone when none is given.
* @param {string} time HH:MM:SS with optional fractional seconds and Z or
* a signed HH:MM offset. Surrounding whitespace is trimmed before parsing
* @returns {{seconds: number, fraction: string, offset: number|null}|null}
* Parsed time, or null for an invalid value. seconds counts whole seconds
* from midnight (86400 for 24:00:00); fraction contains the digits after the
* decimal point, or ""; offset is seconds ahead of UTC, or null if no time
* zone is given
* @see https://eml.ecoinformatics.org/schema/eml-coverage_xsd
* @see https://www.w3.org/TR/xmlschema-2/#time
* @since 2.41.0
*/
parseTime(time) {
// Match the whole HH:MM:SS value, with optional .fraction and optional
// Z or ±HH:MM offset. Clock hours allow 00-24, offset hours allow 00-14,
// and minutes/seconds allow 00-59. Check the special 24/14 rules below.
const match = time
.trim()
.match(
/^([01]\d|2[0-4]):([0-5]\d):([0-5]\d)(?:\.(\d+))?(Z|([+-])(0\d|1[0-4]):([0-5]\d))?$/,
);
if (!match) return null;
const [
,
hours,
minutes,
seconds,
fraction = "",
zone,
sign,
offHours,
offMinutes,
] = match;
// XML Schema allows hour 24 and offset hour 14 only when the remaining
// components are zero; the regex alone does not enforce these limits.
if (
(hours === "24" &&
(minutes !== "00" || seconds !== "00" || /[1-9]/.test(fraction))) ||
(offHours === "14" && offMinutes !== "00")
) {
return null;
}
let offset = null;
if (zone === "Z") offset = 0;
else if (zone) {
offset = (sign === "-" ? -1 : 1) * (offHours * 3600 + offMinutes * 60);
}
return {
seconds: hours * 3600 + minutes * 60 + Number(seconds),
fraction,
offset,
};
},
/**
* Clean text so it can be safely serialized into EML XML.
* @param {*} value Candidate text value.
* @returns {*} Cleaned text, or the original non-string value.
* @since 2.39.0
*/
cleanXMLText(value) {
if (typeof value !== "string") return value;
const text = value
.trim()
.replace(/<\s*[^>]*>/g, (xmlNode) =>
xmlNode.replace(/>/g, ">").replace(/</g, "<"),
);
return XMLUtilities.removeInvalidXmlCharacters(text);
},
/**
* Climbs up the model hierarchy until it finds the EML model
* @param {Backbone.Model} model - The starting model
* @param {number} [maxTries] - The maximum number of levels to climb
* @returns {EML211|false} - Returns the EML 211 Model or null if not found
*/
getParentEML(model, maxTries = 6) {
let emlModel = model.get("parentModel");
let tries = 0;
while (emlModel && emlModel.type !== "EML" && tries < maxTries) {
emlModel = emlModel.get("parentModel");
tries += 1;
}
return emlModel && emlModel.type === "EML" ? emlModel : false;
},
/**
* Serialize a DOM element to a string, removing the XML declaration and any
* namespace declarations. Namespaces are added to the elements because the
* codebase currently combines older ways of handling XML (using jQuery's
* parseHTML and document.createElement) with newer ways (using DOMParser).
* While we transition to using modern methods, this function can be used to
* clean up the serialized XML.
* @param {Element} dom - The DOM element to serialize
* @returns {string} The serialized XML string
* @since 2.35.0
*/
serializeDOM(dom) {
const serializer = new XMLSerializer();
let str = serializer.serializeToString(dom);
// Remove the XML declaration if it exists
str = str.replace(/<\?xml.*?\?>/g, "");
// Remove any namespace declarations
str = str.replace(/xmlns(:\w+)?="[^"]*"/g, "");
return str;
},
/**
* Mark the active package as edited after an EML model or view change.
* @since 2.39.0
*/
markRootDataPackageChanged() {
const dataPackage = MetacatUI.rootDataPackage;
if (typeof dataPackage?.recordUserEdit === "function") {
if (!dataPackage.isEditLocked?.()) {
dataPackage.recordUserEdit("metadata:changed", {});
}
return;
}
// Older editor paths expose a collection with a packageModel.
dataPackage?.packageModel?.set?.("changed", true);
},
};
return EMLUtilities;
});