/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* * Based on the Natural Sort algorithm for Javascript - Version 0.8.1 - adapted * for Firefox DevTools and released under the MIT license. * * Author: Jim Palmer (based on chunking idea from Dave Koelle) * * Repository: * https://github.com/overset/javascript-natural-sort/ */ "use strict"; const tokenizeNumbersRx = /(^([+\-]?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?(?=\D|\s|$))|^0x[\da-fA-F]+$|\d+)/g; const hexRx = /^0x[0-9a-f]+$/i; const startsWithNullRx = /^\0/; const endsWithNullRx = /\0$/; const whitespaceRx = /\s+/g; const startsWithZeroRx = /^0/; const versionRx = /^([\w-]+-)?\d+\.\d+\.\d+$/; const numericDateRx = /^\d+[- /]\d+[- /]\d+$/; // If a string contains any of these, we'll try to parse it as a Date const dateKeywords = [ "mon", "tues", "wed", "thur", "fri", "sat", "sun", "jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec", ]; /** * Figures whether a given string should be considered by naturalSort to be a * Date, and returns the Date's timestamp if so. Some Date formats, like * single numbers and MM.DD.YYYY, are not supported due to conflicts with things * like version numbers. */ function tryParseDate(str) { const lowerCaseStr = str.toLowerCase(); return ( !versionRx.test(str) && (numericDateRx.test(str) || dateKeywords.some(s => lowerCaseStr.includes(s))) && Date.parse(str) ); } /** * Sort numbers, strings, IP Addresses, Dates, Filenames, version numbers etc. * "the way humans do." * * @param {Object} a * Passed in by Array.sort(a, b) * @param {Object} b * Passed in by Array.sort(a, b) * @param {String} sessionString * Client-side value of storage-expires-session l10n string. * Since this function can be called from both the client and the server, * and given that client and server might have different locale, we can't compute * the localized string directly from here. * @param {Boolean} insensitive * Should the search be case insensitive? */ // eslint-disable-next-line complexity function naturalSort(a = "", b = "", sessionString, insensitive = false) { // Ensure we are working with trimmed strings a = (a + "").trim(); b = (b + "").trim(); if (insensitive) { a = a.toLowerCase(); b = b.toLowerCase(); sessionString = sessionString.toLowerCase(); } // Chunk/tokenize - Here we split the strings into arrays or strings and // numbers. const aChunks = a .replace(tokenizeNumbersRx, "\0$1\0") .replace(startsWithNullRx, "") .replace(endsWithNullRx, "") .split("\0"); const bChunks = b .replace(tokenizeNumbersRx, "\0$1\0") .replace(startsWithNullRx, "") .replace(endsWithNullRx, "") .split("\0"); // Hex or date detection. const aHexOrDate = parseInt(a.match(hexRx), 16) || tryParseDate(a); const bHexOrDate = parseInt(b.match(hexRx), 16) || tryParseDate(b); if ( (aHexOrDate || bHexOrDate) && (a === sessionString || b === sessionString) ) { // We have a date and a session string. Move "Session" above the date // (for session cookies) if (a === sessionString) { return -1; } else if (b === sessionString) { return 1; } } // Try and sort Hex codes or Dates. if (aHexOrDate && bHexOrDate) { if (aHexOrDate < bHexOrDate) { return -1; } else if (aHexOrDate > bHexOrDate) { return 1; } return 0; } // Natural sorting through split numeric strings and default strings const aChunksLength = aChunks.length; const bChunksLength = bChunks.length; const maxLen = Math.max(aChunksLength, bChunksLength); for (let i = 0; i < maxLen; i++) { const aChunk = normalizeChunk(aChunks[i] || "", aChunksLength); const bChunk = normalizeChunk(bChunks[i] || "", bChunksLength); // Handle numeric vs string comparison - number < string if (isNaN(aChunk) !== isNaN(bChunk)) { return isNaN(aChunk) ? 1 : -1; } // If unicode use locale comparison // eslint-disable-next-line no-control-regex if (/[^\x00-\x80]/.test(aChunk + bChunk) && aChunk.localeCompare) { const comp = aChunk.localeCompare(bChunk); return comp / Math.abs(comp); } if (aChunk < bChunk) { return -1; } else if (aChunk > bChunk) { return 1; } } return null; } // Normalize spaces; find floats not starting with '0', string or 0 if not // defined const normalizeChunk = function (str, length) { return ( ((!str.match(startsWithZeroRx) || length == 1) && parseFloat(str)) || str.replace(whitespaceRx, " ").trim() || 0 ); }; exports.naturalSortCaseSensitive = function naturalSortCaseSensitive( a, b, sessionString ) { return naturalSort(a, b, sessionString, false); }; exports.naturalSortCaseInsensitive = function naturalSortCaseInsensitive( a, b, sessionString ) { return naturalSort(a, b, sessionString, true); };