diff options
Diffstat (limited to 'node_modules/bcp-47-match/index.js')
| -rw-r--r-- | node_modules/bcp-47-match/index.js | 234 |
1 files changed, 234 insertions, 0 deletions
diff --git a/node_modules/bcp-47-match/index.js b/node_modules/bcp-47-match/index.js new file mode 100644 index 0000000..6d12bfc --- /dev/null +++ b/node_modules/bcp-47-match/index.js @@ -0,0 +1,234 @@ +/** + * See <https://tools.ietf.org/html/rfc4647#section-3.1> + * for more info on the algorithms. + */ + +/** + * @typedef {string} Tag + * BCP-47 tag. + * @typedef {Array<Tag>} Tags + * List of BCP-47 tags. + * @typedef {string} Range + * RFC 4647 range. + * @typedef {Array<Range>} 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<true>} Filter + * Filter: yields all tags that match a range. + * @typedef {FilterOrLookup<false>} 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<IsFilter>} + * 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<string>} values + * @param {string} name + * @returns {Array<string>} + */ +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 +} |
