|
1 |
| -// See https://tools.ietf.org/html/rfc4647#section-3.1 |
2 |
| -// for more information on the algorithms. |
3 |
| - |
4 |
| -export var basicFilter = factory(basic, true) |
5 |
| -export var extendedFilter = factory(extended, true) |
6 |
| -export var lookup = factory(look) |
7 |
| - |
8 |
| -// Basic Filtering (Section 3.3.1) matches a language priority list consisting |
9 |
| -// of basic language ranges (Section 2.1) to sets of language tags. |
10 |
| -function basic(tag, range) { |
11 |
| - return range === '*' || tag === range || tag.includes(range + '-') |
12 |
| -} |
13 |
| - |
14 |
| -// Extended Filtering (Section 3.3.2) matches a language priority list |
15 |
| -// consisting of extended language ranges (Section 2.2) to sets of language |
16 |
| -// tags. |
17 |
| -function extended(tag, range) { |
18 |
| - // 3.3.2.1 |
19 |
| - var left = tag.split('-') |
20 |
| - var right = range.split('-') |
21 |
| - var leftIndex = 0 |
22 |
| - var rightIndex = 0 |
23 |
| - |
24 |
| - // 3.3.2.2 |
25 |
| - if (right[rightIndex] !== '*' && left[leftIndex] !== right[rightIndex]) { |
26 |
| - return false |
27 |
| - } |
| 1 | +/** |
| 2 | + * See https://tools.ietf.org/html/rfc4647#section-3.1 |
| 3 | + * for more information on the algorithms. |
| 4 | + */ |
| 5 | + |
| 6 | +/** |
| 7 | + * @typedef {string} Tag |
| 8 | + * @typedef {Array.<Tag>} Tags |
| 9 | + * @typedef {string} Range |
| 10 | + * @typedef {Array.<Range>} Ranges |
| 11 | + * @typedef {function(Tag, Range): boolean} Check |
| 12 | + * @typedef {function(Tag|Tags, Range|Ranges=): Tags} Filter |
| 13 | + * @typedef {function(Tag|Tags, Range|Ranges=): Tag} Lookup |
| 14 | + */ |
| 15 | + |
| 16 | +/** |
| 17 | + * Factory to perform a filter or a lookup. |
| 18 | + * This factory creates a function that accepts a list of tags and a list of |
| 19 | + * ranges, and contains logic to exit early for lookups. |
| 20 | + * `check` just has to deal with one tag and one range. |
| 21 | + * This match function iterates over ranges, and for each range, |
| 22 | + * iterates over tags. That way, earlier ranges matching any tag have |
| 23 | + * precedence over later ranges. |
| 24 | + * |
| 25 | + * @type {{ |
| 26 | + * (check: Check, filter: true): Filter |
| 27 | + * (check: Check, filter?: false): Lookup |
| 28 | + * }} |
| 29 | + */ |
| 30 | +// prettier-ignore |
| 31 | +var factory = ( |
| 32 | + /** |
| 33 | + * @param {Check} check |
| 34 | + * @param {boolean} [filter=false] |
| 35 | + */ |
| 36 | + function (check, filter) { |
| 37 | + return match |
| 38 | + |
| 39 | + /** |
| 40 | + * @param {Tag|Tags} tags |
| 41 | + * @param {Range|Ranges} [ranges='*'] |
| 42 | + * @returns {Tag|Tags} |
| 43 | + */ |
| 44 | + function match(tags, ranges) { |
| 45 | + var left = cast(tags, 'tag') |
| 46 | + var right = cast( |
| 47 | + ranges === null || ranges === undefined ? '*' : ranges, |
| 48 | + 'range' |
| 49 | + ) |
| 50 | + /** @type {Tags} */ |
| 51 | + var matches = [] |
| 52 | + var rightIndex = -1 |
| 53 | + /** @type {Range} */ |
| 54 | + var range |
| 55 | + /** @type {number} */ |
| 56 | + var leftIndex |
| 57 | + /** @type {Tags} */ |
| 58 | + var next |
| 59 | + |
| 60 | + while (++rightIndex < right.length) { |
| 61 | + range = right[rightIndex].toLowerCase() |
| 62 | + |
| 63 | + // Ignore wildcards in lookup mode. |
| 64 | + if (!filter && range === '*') continue |
| 65 | + |
| 66 | + leftIndex = -1 |
| 67 | + next = [] |
| 68 | + |
| 69 | + while (++leftIndex < left.length) { |
| 70 | + if (check(left[leftIndex].toLowerCase(), range)) { |
| 71 | + // Exit if this is a lookup and we have a match. |
| 72 | + if (!filter) return left[leftIndex] |
| 73 | + matches.push(left[leftIndex]) |
| 74 | + } else { |
| 75 | + next.push(left[leftIndex]) |
| 76 | + } |
| 77 | + } |
28 | 78 |
|
29 |
| - leftIndex++ |
30 |
| - rightIndex++ |
| 79 | + left = next |
| 80 | + } |
31 | 81 |
|
32 |
| - // 3.3.2.3 |
33 |
| - while (rightIndex < right.length) { |
34 |
| - // 3.3.2.3.A |
35 |
| - if (right[rightIndex] === '*') { |
36 |
| - rightIndex++ |
37 |
| - continue |
| 82 | + // If this is a filter, return the list. If it’s a lookup, we didn’t find |
| 83 | + // a match, so return `undefined`. |
| 84 | + return filter ? matches : undefined |
38 | 85 | }
|
39 |
| - |
40 |
| - // 3.3.2.3.B |
41 |
| - if (!left[leftIndex]) return false |
42 |
| - |
43 |
| - // 3.3.2.3.C |
44 |
| - if (left[leftIndex] === right[rightIndex]) { |
45 |
| - leftIndex++ |
46 |
| - rightIndex++ |
47 |
| - continue |
| 86 | + } |
| 87 | +) |
| 88 | + |
| 89 | +/** |
| 90 | + * Basic Filtering (Section 3.3.1) matches a language priority list consisting |
| 91 | + * of basic language ranges (Section 2.1) to sets of language tags. |
| 92 | + * @param {Tag|Tags} tags |
| 93 | + * @param {Range|Ranges} [ranges] |
| 94 | + * @returns {Tags} |
| 95 | + */ |
| 96 | +export var basicFilter = factory( |
| 97 | + /** @type {Check} */ |
| 98 | + function (tag, range) { |
| 99 | + return range === '*' || tag === range || tag.includes(range + '-') |
| 100 | + }, |
| 101 | + true |
| 102 | +) |
| 103 | + |
| 104 | +/** |
| 105 | + * Extended Filtering (Section 3.3.2) matches a language priority list |
| 106 | + * consisting of extended language ranges (Section 2.2) to sets of language |
| 107 | + * tags. |
| 108 | + * @param {Tag|Tags} tags |
| 109 | + * @param {Range|Ranges} [ranges] |
| 110 | + * @returns {Tags} |
| 111 | + */ |
| 112 | +export var extendedFilter = factory( |
| 113 | + /** @type {Check} */ |
| 114 | + function (tag, range) { |
| 115 | + // 3.3.2.1 |
| 116 | + var left = tag.split('-') |
| 117 | + var right = range.split('-') |
| 118 | + var leftIndex = 0 |
| 119 | + var rightIndex = 0 |
| 120 | + |
| 121 | + // 3.3.2.2 |
| 122 | + if (right[rightIndex] !== '*' && left[leftIndex] !== right[rightIndex]) { |
| 123 | + return false |
48 | 124 | }
|
49 | 125 |
|
50 |
| - // 3.3.2.3.D |
51 |
| - if (left[leftIndex].length === 1) return false |
52 |
| - |
53 |
| - // 3.3.2.3.E |
54 | 126 | leftIndex++
|
55 |
| - } |
56 |
| - |
57 |
| - // 3.3.2.4 |
58 |
| - return true |
59 |
| -} |
60 |
| - |
61 |
| -// Lookup (Section 3.4) matches a language priority list consisting of basic |
62 |
| -// language ranges to sets of language tags to find the one exact language tag |
63 |
| -// that best matches the range. |
64 |
| -function look(tag, range) { |
65 |
| - var right = range |
66 |
| - var index |
67 |
| - |
68 |
| - /* eslint-disable-next-line no-constant-condition */ |
69 |
| - while (true) { |
70 |
| - if (right === '*' || tag === right) return true |
71 |
| - |
72 |
| - index = right.lastIndexOf('-') |
73 |
| - |
74 |
| - if (index < 0) return false |
75 |
| - |
76 |
| - if (right.charAt(index - 2) === '-') index -= 2 |
| 127 | + rightIndex++ |
| 128 | + |
| 129 | + // 3.3.2.3 |
| 130 | + while (rightIndex < right.length) { |
| 131 | + // 3.3.2.3.A |
| 132 | + if (right[rightIndex] === '*') { |
| 133 | + rightIndex++ |
| 134 | + continue |
| 135 | + } |
77 | 136 |
|
78 |
| - right = right.slice(0, index) |
79 |
| - } |
80 |
| -} |
| 137 | + // 3.3.2.3.B |
| 138 | + if (!left[leftIndex]) return false |
81 | 139 |
|
82 |
| -// Factory to perform a filter or a lookup. |
83 |
| -// This factory creates a function that accepts a list of tags and a list of |
84 |
| -// ranges, and contains logic to exit early for lookups. |
85 |
| -// `check` just has to deal with one tag and one range. |
86 |
| -// This match function iterates over ranges, and for each range, |
87 |
| -// iterates over tags. That way, earlier ranges matching any tag have |
88 |
| -// precedence over later ranges. |
89 |
| -function factory(check, filter) { |
90 |
| - return match |
91 |
| - |
92 |
| - function match(tags, ranges) { |
93 |
| - var left = cast(tags, 'tag') |
94 |
| - var right = cast( |
95 |
| - ranges === null || ranges === undefined ? '*' : ranges, |
96 |
| - 'range' |
97 |
| - ) |
98 |
| - var matches = [] |
99 |
| - var rightIndex = -1 |
100 |
| - var range |
101 |
| - var leftIndex |
102 |
| - var next |
103 |
| - |
104 |
| - while (++rightIndex < right.length) { |
105 |
| - range = right[rightIndex].toLowerCase() |
106 |
| - |
107 |
| - // Ignore wildcards in lookup mode. |
108 |
| - if (!filter && range === '*') continue |
109 |
| - |
110 |
| - leftIndex = -1 |
111 |
| - next = [] |
112 |
| - |
113 |
| - while (++leftIndex < left.length) { |
114 |
| - if (check(left[leftIndex].toLowerCase(), range)) { |
115 |
| - // Exit if this is a lookup and we have a match. |
116 |
| - if (!filter) return left[leftIndex] |
117 |
| - matches.push(left[leftIndex]) |
118 |
| - } else { |
119 |
| - next.push(left[leftIndex]) |
120 |
| - } |
| 140 | + // 3.3.2.3.C |
| 141 | + if (left[leftIndex] === right[rightIndex]) { |
| 142 | + leftIndex++ |
| 143 | + rightIndex++ |
| 144 | + continue |
121 | 145 | }
|
122 | 146 |
|
123 |
| - left = next |
| 147 | + // 3.3.2.3.D |
| 148 | + if (left[leftIndex].length === 1) return false |
| 149 | + |
| 150 | + // 3.3.2.3.E |
| 151 | + leftIndex++ |
124 | 152 | }
|
125 | 153 |
|
126 |
| - // If this is a filter, return the list. If it’s a lookup, we didn’t find |
127 |
| - // a match, so return `undefined`. |
128 |
| - return filter ? matches : undefined |
| 154 | + // 3.3.2.4 |
| 155 | + return true |
| 156 | + }, |
| 157 | + true |
| 158 | +) |
| 159 | + |
| 160 | +/** |
| 161 | + * Lookup (Section 3.4) matches a language priority list consisting of basic |
| 162 | + * language ranges to sets of language tags to find the one exact language tag |
| 163 | + * that best matches the range. |
| 164 | + * @param {Tag|Tags} tags |
| 165 | + * @param {Range|Ranges} [ranges] |
| 166 | + * @returns {Tag} |
| 167 | + */ |
| 168 | +export var lookup = factory( |
| 169 | + /** @type {Check} */ |
| 170 | + function (tag, range) { |
| 171 | + var right = range |
| 172 | + /** @type {number} */ |
| 173 | + var index |
| 174 | + |
| 175 | + /* eslint-disable-next-line no-constant-condition */ |
| 176 | + while (true) { |
| 177 | + if (right === '*' || tag === right) return true |
| 178 | + |
| 179 | + index = right.lastIndexOf('-') |
| 180 | + |
| 181 | + if (index < 0) return false |
| 182 | + |
| 183 | + if (right.charAt(index - 2) === '-') index -= 2 |
| 184 | + |
| 185 | + right = right.slice(0, index) |
| 186 | + } |
129 | 187 | }
|
130 |
| -} |
131 |
| - |
132 |
| -// Validate tags or ranges, and cast them to arrays. |
| 188 | +) |
| 189 | + |
| 190 | +/** |
| 191 | + * Validate tags or ranges, and cast them to arrays. |
| 192 | + * |
| 193 | + * @param {string|Array.<string>} values |
| 194 | + * @param {string} name |
| 195 | + * @returns {Array.<string>} |
| 196 | + */ |
133 | 197 | function cast(values, name) {
|
134 | 198 | var value = values && typeof values === 'string' ? [values] : values
|
135 | 199 |
|
|
0 commit comments