/** * See * for more info on the algorithms. */ /** * @typedef {string} Tag * BCP-47 tag. * @typedef {Array} Tags * List of BCP-47 tags. * @typedef {string} Range * RFC 4647 range. * @typedef {Array} Ranges * List of RFC 4647 range. * * @callback Check * An internal check. * @param {Tag} tag * BCP-47 tag. * @param {Range} range * RFC 4647 range. * @returns {boolean} * Whether the range matches the tag. * * @typedef {FilterOrLookup} Filter * Filter: yields all tags that match a range. * @typedef {FilterOrLookup} Lookup * Lookup: yields the best tag that matches a range. */ /** * @template {boolean} IsFilter * Whether to filter or perform a lookup. * @callback FilterOrLookup * A check. * @param {Tag|Tags} tags * One or more BCP-47 tags. * @param {Range|Ranges|undefined} [ranges='*'] * One or more RFC 4647 ranges. * @returns {IsFilter extends true ? Tags : Tag|undefined} * Result. */ /** * Factory to perform a filter or a lookup. * * This factory creates a function that accepts a list of tags and a list of * ranges, and contains logic to exit early for lookups. * `check` just has to deal with one tag and one range. * This match function iterates over ranges, and for each range, * iterates over tags. * That way, earlier ranges matching any tag have precedence over later ranges. * * @template {boolean} IsFilter * @param {Check} check * A check. * @param {IsFilter} filter * Whether to filter or perform a lookup. * @returns {FilterOrLookup} * Filter or lookup. */ function factory(check, filter) { /** * @param {Tag|Tags} tags * One or more BCP-47 tags. * @param {Range|Ranges|undefined} [ranges='*'] * One or more RFC 4647 ranges. * @returns {IsFilter extends true ? Tags : Tag|undefined} * Result. */ return function (tags, ranges) { let left = cast(tags, 'tag') const right = cast( ranges === null || ranges === undefined ? '*' : ranges, 'range' ) /** @type {Tags} */ const matches = [] let rightIndex = -1 while (++rightIndex < right.length) { const range = right[rightIndex].toLowerCase() // Ignore wildcards in lookup mode. if (!filter && range === '*') continue let leftIndex = -1 /** @type {Tags} */ const next = [] while (++leftIndex < left.length) { if (check(left[leftIndex].toLowerCase(), range)) { // Exit if this is a lookup and we have a match. if (!filter) { return /** @type {IsFilter extends true ? Tags : Tag|undefined} */ ( left[leftIndex] ) } matches.push(left[leftIndex]) } else { next.push(left[leftIndex]) } } left = next } // If this is a filter, return the list. If it’s a lookup, we didn’t find // a match, so return `undefined`. return /** @type {IsFilter extends true ? Tags : Tag|undefined} */ ( filter ? matches : undefined ) } } /** * Basic Filtering (Section 3.3.1) matches a language priority list consisting * of basic language ranges (Section 2.1) to sets of language tags. * * @param {Tag|Tags} tags * One or more BCP-47 tags. * @param {Range|Ranges|undefined} [ranges='*'] * One or more RFC 4647 ranges. * @returns {Tags} * List of BCP-47 tags. */ export const basicFilter = factory(function (tag, range) { return range === '*' || tag === range || tag.includes(range + '-') }, true) /** * Extended Filtering (Section 3.3.2) matches a language priority list * consisting of extended language ranges (Section 2.2) to sets of language * tags. * * @param {Tag|Tags} tags * One or more BCP-47 tags. * @param {Range|Ranges|undefined} [ranges='*'] * One or more RFC 4647 ranges. * @returns {Tags} * List of BCP-47 tags. */ export const extendedFilter = factory(function (tag, range) { // 3.3.2.1 const left = tag.split('-') const right = range.split('-') let leftIndex = 0 let rightIndex = 0 // 3.3.2.2 if (right[rightIndex] !== '*' && left[leftIndex] !== right[rightIndex]) { return false } leftIndex++ rightIndex++ // 3.3.2.3 while (rightIndex < right.length) { // 3.3.2.3.A if (right[rightIndex] === '*') { rightIndex++ continue } // 3.3.2.3.B if (!left[leftIndex]) return false // 3.3.2.3.C if (left[leftIndex] === right[rightIndex]) { leftIndex++ rightIndex++ continue } // 3.3.2.3.D if (left[leftIndex].length === 1) return false // 3.3.2.3.E leftIndex++ } // 3.3.2.4 return true }, true) /** * Lookup (Section 3.4) matches a language priority list consisting of basic * language ranges to sets of language tags to find the one exact language tag * that best matches the range. * * @param {Tag|Tags} tags * One or more BCP-47 tags. * @param {Range|Ranges|undefined} [ranges='*'] * One or more RFC 4647 ranges. * @returns {Tag|undefined} * BCP-47 tag. */ export const lookup = factory(function (tag, range) { let right = range /* eslint-disable-next-line no-constant-condition */ while (true) { if (right === '*' || tag === right) return true let index = right.lastIndexOf('-') if (index < 0) return false if (right.charAt(index - 2) === '-') index -= 2 right = right.slice(0, index) } }, false) /** * Validate tags or ranges, and cast them to arrays. * * @param {string|Array} values * @param {string} name * @returns {Array} */ function cast(values, name) { const value = values && typeof values === 'string' ? [values] : values if (!value || typeof value !== 'object' || !('length' in value)) { throw new Error( 'Invalid ' + name + ' `' + value + '`, expected non-empty string' ) } return value }