source: frontend/node_modules/tr46/index.js

Last change on this file was 9af201e, checked in by MBK <marija.karapandzova@…>, 11 days ago

Fix frontend appearance

  • Property mode set to 100644
File size: 7.2 KB
Line 
1"use strict";
2
3const punycode = require("punycode");
4const regexes = require("./lib/regexes.js");
5const mappingTable = require("./lib/mappingTable.json");
6const { STATUS_MAPPING } = require("./lib/statusMapping.js");
7
8function containsNonASCII(str) {
9 return /[^\x00-\x7F]/.test(str);
10}
11
12function findStatus(val, { useSTD3ASCIIRules }) {
13 let start = 0;
14 let end = mappingTable.length - 1;
15
16 while (start <= end) {
17 const mid = Math.floor((start + end) / 2);
18
19 const target = mappingTable[mid];
20 const min = Array.isArray(target[0]) ? target[0][0] : target[0];
21 const max = Array.isArray(target[0]) ? target[0][1] : target[0];
22
23 if (min <= val && max >= val) {
24 if (useSTD3ASCIIRules &&
25 (target[1] === STATUS_MAPPING.disallowed_STD3_valid || target[1] === STATUS_MAPPING.disallowed_STD3_mapped)) {
26 return [STATUS_MAPPING.disallowed, ...target.slice(2)];
27 } else if (target[1] === STATUS_MAPPING.disallowed_STD3_valid) {
28 return [STATUS_MAPPING.valid, ...target.slice(2)];
29 } else if (target[1] === STATUS_MAPPING.disallowed_STD3_mapped) {
30 return [STATUS_MAPPING.mapped, ...target.slice(2)];
31 }
32
33 return target.slice(1);
34 } else if (min > val) {
35 end = mid - 1;
36 } else {
37 start = mid + 1;
38 }
39 }
40
41 return null;
42}
43
44function mapChars(domainName, { useSTD3ASCIIRules, processingOption }) {
45 let hasError = false;
46 let processed = "";
47
48 for (const ch of domainName) {
49 const [status, mapping] = findStatus(ch.codePointAt(0), { useSTD3ASCIIRules });
50
51 switch (status) {
52 case STATUS_MAPPING.disallowed:
53 hasError = true;
54 processed += ch;
55 break;
56 case STATUS_MAPPING.ignored:
57 break;
58 case STATUS_MAPPING.mapped:
59 processed += mapping;
60 break;
61 case STATUS_MAPPING.deviation:
62 if (processingOption === "transitional") {
63 processed += mapping;
64 } else {
65 processed += ch;
66 }
67 break;
68 case STATUS_MAPPING.valid:
69 processed += ch;
70 break;
71 }
72 }
73
74 return {
75 string: processed,
76 error: hasError
77 };
78}
79
80function validateLabel(label, { checkHyphens, checkBidi, checkJoiners, processingOption, useSTD3ASCIIRules }) {
81 if (label.normalize("NFC") !== label) {
82 return false;
83 }
84
85 const codePoints = Array.from(label);
86
87 if (checkHyphens) {
88 if ((codePoints[2] === "-" && codePoints[3] === "-") ||
89 (label.startsWith("-") || label.endsWith("-"))) {
90 return false;
91 }
92 }
93
94 if (label.includes(".") ||
95 (codePoints.length > 0 && regexes.combiningMarks.test(codePoints[0]))) {
96 return false;
97 }
98
99 for (const ch of codePoints) {
100 const [status] = findStatus(ch.codePointAt(0), { useSTD3ASCIIRules });
101 if ((processingOption === "transitional" && status !== STATUS_MAPPING.valid) ||
102 (processingOption === "nontransitional" &&
103 status !== STATUS_MAPPING.valid && status !== STATUS_MAPPING.deviation)) {
104 return false;
105 }
106 }
107
108 // https://tools.ietf.org/html/rfc5892#appendix-A
109 if (checkJoiners) {
110 let last = 0;
111 for (const [i, ch] of codePoints.entries()) {
112 if (ch === "\u200C" || ch === "\u200D") {
113 if (i > 0) {
114 if (regexes.combiningClassVirama.test(codePoints[i - 1])) {
115 continue;
116 }
117 if (ch === "\u200C") {
118 // TODO: make this more efficient
119 const next = codePoints.indexOf("\u200C", i + 1);
120 const test = next < 0 ? codePoints.slice(last) : codePoints.slice(last, next);
121 if (regexes.validZWNJ.test(test.join(""))) {
122 last = i + 1;
123 continue;
124 }
125 }
126 }
127 return false;
128 }
129 }
130 }
131
132 // https://tools.ietf.org/html/rfc5893#section-2
133 if (checkBidi) {
134 let rtl;
135
136 // 1
137 if (regexes.bidiS1LTR.test(codePoints[0])) {
138 rtl = false;
139 } else if (regexes.bidiS1RTL.test(codePoints[0])) {
140 rtl = true;
141 } else {
142 return false;
143 }
144
145 if (rtl) {
146 // 2-4
147 if (!regexes.bidiS2.test(label) ||
148 !regexes.bidiS3.test(label) ||
149 (regexes.bidiS4EN.test(label) && regexes.bidiS4AN.test(label))) {
150 return false;
151 }
152 } else if (!regexes.bidiS5.test(label) ||
153 !regexes.bidiS6.test(label)) { // 5-6
154 return false;
155 }
156 }
157
158 return true;
159}
160
161function isBidiDomain(labels) {
162 const domain = labels.map(label => {
163 if (label.startsWith("xn--")) {
164 try {
165 return punycode.decode(label.substring(4));
166 } catch (err) {
167 return "";
168 }
169 }
170 return label;
171 }).join(".");
172 return regexes.bidiDomain.test(domain);
173}
174
175function processing(domainName, options) {
176 const { processingOption } = options;
177
178 // 1. Map.
179 let { string, error } = mapChars(domainName, options);
180
181 // 2. Normalize.
182 string = string.normalize("NFC");
183
184 // 3. Break.
185 const labels = string.split(".");
186 const isBidi = isBidiDomain(labels);
187
188 // 4. Convert/Validate.
189 for (const [i, origLabel] of labels.entries()) {
190 let label = origLabel;
191 let curProcessing = processingOption;
192 if (label.startsWith("xn--")) {
193 try {
194 label = punycode.decode(label.substring(4));
195 labels[i] = label;
196 } catch (err) {
197 error = true;
198 continue;
199 }
200 curProcessing = "nontransitional";
201 }
202
203 // No need to validate if we already know there is an error.
204 if (error) {
205 continue;
206 }
207 const validation = validateLabel(label, Object.assign({}, options, {
208 processingOption: curProcessing,
209 checkBidi: options.checkBidi && isBidi
210 }));
211 if (!validation) {
212 error = true;
213 }
214 }
215
216 return {
217 string: labels.join("."),
218 error
219 };
220}
221
222function toASCII(domainName, {
223 checkHyphens = false,
224 checkBidi = false,
225 checkJoiners = false,
226 useSTD3ASCIIRules = false,
227 processingOption = "nontransitional",
228 verifyDNSLength = false
229} = {}) {
230 if (processingOption !== "transitional" && processingOption !== "nontransitional") {
231 throw new RangeError("processingOption must be either transitional or nontransitional");
232 }
233
234 const result = processing(domainName, {
235 processingOption,
236 checkHyphens,
237 checkBidi,
238 checkJoiners,
239 useSTD3ASCIIRules
240 });
241 let labels = result.string.split(".");
242 labels = labels.map(l => {
243 if (containsNonASCII(l)) {
244 try {
245 return "xn--" + punycode.encode(l);
246 } catch (e) {
247 result.error = true;
248 }
249 }
250 return l;
251 });
252
253 if (verifyDNSLength) {
254 const total = labels.join(".").length;
255 if (total > 253 || total === 0) {
256 result.error = true;
257 }
258
259 for (let i = 0; i < labels.length; ++i) {
260 if (labels[i].length > 63 || labels[i].length === 0) {
261 result.error = true;
262 break;
263 }
264 }
265 }
266
267 if (result.error) {
268 return null;
269 }
270 return labels.join(".");
271}
272
273function toUnicode(domainName, {
274 checkHyphens = false,
275 checkBidi = false,
276 checkJoiners = false,
277 useSTD3ASCIIRules = false,
278 processingOption = "nontransitional"
279} = {}) {
280 const result = processing(domainName, {
281 processingOption,
282 checkHyphens,
283 checkBidi,
284 checkJoiners,
285 useSTD3ASCIIRules
286 });
287
288 return {
289 domain: result.string,
290 error: result.error
291 };
292}
293
294module.exports = {
295 toASCII,
296 toUnicode
297};
Note: See TracBrowser for help on using the repository browser.