aboutsummaryrefslogtreecommitdiffstats
path: root/vanilla/node_modules/tr46/index.js
diff options
context:
space:
mode:
authorAdam Mathes <adam@adammathes.com>2026-02-13 21:34:48 -0800
committerAdam Mathes <adam@adammathes.com>2026-02-13 21:34:48 -0800
commit76cb9c2a39d477a64824a985ade40507e3bbade1 (patch)
tree41e997aa9c6f538d3a136af61dae9424db2005a9 /vanilla/node_modules/tr46/index.js
parent819a39a21ac992b1393244a4c283bbb125208c69 (diff)
downloadneko-76cb9c2a39d477a64824a985ade40507e3bbade1.tar.gz
neko-76cb9c2a39d477a64824a985ade40507e3bbade1.tar.bz2
neko-76cb9c2a39d477a64824a985ade40507e3bbade1.zip
feat(vanilla): add testing infrastructure and tests (NK-wjnczv)
Diffstat (limited to 'vanilla/node_modules/tr46/index.js')
-rw-r--r--vanilla/node_modules/tr46/index.js344
1 files changed, 344 insertions, 0 deletions
diff --git a/vanilla/node_modules/tr46/index.js b/vanilla/node_modules/tr46/index.js
new file mode 100644
index 0000000..9e53f05
--- /dev/null
+++ b/vanilla/node_modules/tr46/index.js
@@ -0,0 +1,344 @@
+"use strict";
+
+const punycode = require("punycode/");
+const regexes = require("./lib/regexes.js");
+const mappingTable = require("./lib/mappingTable.json");
+const { STATUS_MAPPING } = require("./lib/statusMapping.js");
+
+function containsNonASCII(str) {
+ return /[^\x00-\x7F]/u.test(str);
+}
+
+function findStatus(val) {
+ let start = 0;
+ let end = mappingTable.length - 1;
+
+ while (start <= end) {
+ const mid = Math.floor((start + end) / 2);
+
+ const target = mappingTable[mid];
+ const min = Array.isArray(target[0]) ? target[0][0] : target[0];
+ const max = Array.isArray(target[0]) ? target[0][1] : target[0];
+
+ if (min <= val && max >= val) {
+ return target.slice(1);
+ } else if (min > val) {
+ end = mid - 1;
+ } else {
+ start = mid + 1;
+ }
+ }
+
+ return null;
+}
+
+function mapChars(domainName, { transitionalProcessing }) {
+ let processed = "";
+
+ for (const ch of domainName) {
+ const [status, mapping] = findStatus(ch.codePointAt(0));
+
+ switch (status) {
+ case STATUS_MAPPING.disallowed:
+ processed += ch;
+ break;
+ case STATUS_MAPPING.ignored:
+ break;
+ case STATUS_MAPPING.mapped:
+ if (transitionalProcessing && ch === "ẞ") {
+ processed += "ss";
+ } else {
+ processed += mapping;
+ }
+ break;
+ case STATUS_MAPPING.deviation:
+ if (transitionalProcessing) {
+ processed += mapping;
+ } else {
+ processed += ch;
+ }
+ break;
+ case STATUS_MAPPING.valid:
+ processed += ch;
+ break;
+ }
+ }
+
+ return processed;
+}
+
+function validateLabel(label, {
+ checkHyphens,
+ checkBidi,
+ checkJoiners,
+ transitionalProcessing,
+ useSTD3ASCIIRules,
+ isBidi
+}) {
+ // "must be satisfied for a non-empty label"
+ if (label.length === 0) {
+ return true;
+ }
+
+ // "1. The label must be in Unicode Normalization Form NFC."
+ if (label.normalize("NFC") !== label) {
+ return false;
+ }
+
+ const codePoints = Array.from(label);
+
+ // "2. If CheckHyphens, the label must not contain a U+002D HYPHEN-MINUS character in both the
+ // third and fourth positions."
+ //
+ // "3. If CheckHyphens, the label must neither begin nor end with a U+002D HYPHEN-MINUS character."
+ if (checkHyphens) {
+ if ((codePoints[2] === "-" && codePoints[3] === "-") ||
+ (label.startsWith("-") || label.endsWith("-"))) {
+ return false;
+ }
+ }
+
+ // "4. If not CheckHyphens, the label must not begin with “xn--”."
+ if (!checkHyphens) {
+ if (label.startsWith("xn--")) {
+ return false;
+ }
+ }
+
+ // "5. The label must not contain a U+002E ( . ) FULL STOP."
+ if (label.includes(".")) {
+ return false;
+ }
+
+ // "6. The label must not begin with a combining mark, that is: General_Category=Mark."
+ if (regexes.combiningMarks.test(codePoints[0])) {
+ return false;
+ }
+
+ // "7. Each code point in the label must only have certain Status values according to Section 5"
+ for (const ch of codePoints) {
+ const codePoint = ch.codePointAt(0);
+ const [status] = findStatus(codePoint);
+ if (transitionalProcessing) {
+ // "For Transitional Processing (deprecated), each value must be valid."
+ if (status !== STATUS_MAPPING.valid) {
+ return false;
+ }
+ } else if (status !== STATUS_MAPPING.valid && status !== STATUS_MAPPING.deviation) {
+ // "For Nontransitional Processing, each value must be either valid or deviation."
+ return false;
+ }
+ // "In addition, if UseSTD3ASCIIRules=true and the code point is an ASCII code point (U+0000..U+007F), then it must
+ // be a lowercase letter (a-z), a digit (0-9), or a hyphen-minus (U+002D). (Note: This excludes uppercase ASCII
+ // A-Z which are mapped in UTS #46 and disallowed in IDNA2008.)"
+ if (useSTD3ASCIIRules && codePoint <= 0x7F) {
+ if (!/^(?:[a-z]|[0-9]|-)$/u.test(ch)) {
+ return false;
+ }
+ }
+ }
+
+ // "8. If CheckJoiners, the label must satisify the ContextJ rules"
+ // https://tools.ietf.org/html/rfc5892#appendix-A
+ if (checkJoiners) {
+ let last = 0;
+ for (const [i, ch] of codePoints.entries()) {
+ if (ch === "\u200C" || ch === "\u200D") {
+ if (i > 0) {
+ if (regexes.combiningClassVirama.test(codePoints[i - 1])) {
+ continue;
+ }
+ if (ch === "\u200C") {
+ // TODO: make this more efficient
+ const next = codePoints.indexOf("\u200C", i + 1);
+ const test = next < 0 ? codePoints.slice(last) : codePoints.slice(last, next);
+ if (regexes.validZWNJ.test(test.join(""))) {
+ last = i + 1;
+ continue;
+ }
+ }
+ }
+ return false;
+ }
+ }
+ }
+
+ // "9. If CheckBidi, and if the domain name is a Bidi domain name, then the label must satisfy..."
+ // https://tools.ietf.org/html/rfc5893#section-2
+ if (checkBidi && isBidi) {
+ let rtl;
+
+ // 1
+ if (regexes.bidiS1LTR.test(codePoints[0])) {
+ rtl = false;
+ } else if (regexes.bidiS1RTL.test(codePoints[0])) {
+ rtl = true;
+ } else {
+ return false;
+ }
+
+ if (rtl) {
+ // 2-4
+ if (!regexes.bidiS2.test(label) ||
+ !regexes.bidiS3.test(label) ||
+ (regexes.bidiS4EN.test(label) && regexes.bidiS4AN.test(label))) {
+ return false;
+ }
+ } else if (!regexes.bidiS5.test(label) ||
+ !regexes.bidiS6.test(label)) { // 5-6
+ return false;
+ }
+ }
+
+ return true;
+}
+
+function isBidiDomain(labels) {
+ const domain = labels.map(label => {
+ if (label.startsWith("xn--")) {
+ try {
+ return punycode.decode(label.substring(4));
+ } catch {
+ return "";
+ }
+ }
+ return label;
+ }).join(".");
+ return regexes.bidiDomain.test(domain);
+}
+
+function processing(domainName, options) {
+ // 1. Map.
+ let string = mapChars(domainName, options);
+
+ // 2. Normalize.
+ string = string.normalize("NFC");
+
+ // 3. Break.
+ const labels = string.split(".");
+ const isBidi = isBidiDomain(labels);
+
+ // 4. Convert/Validate.
+ let error = false;
+ for (const [i, origLabel] of labels.entries()) {
+ let label = origLabel;
+ let transitionalProcessingForThisLabel = options.transitionalProcessing;
+ if (label.startsWith("xn--")) {
+ if (containsNonASCII(label)) {
+ error = true;
+ continue;
+ }
+
+ try {
+ label = punycode.decode(label.substring(4));
+ } catch {
+ if (!options.ignoreInvalidPunycode) {
+ error = true;
+ continue;
+ }
+ }
+ labels[i] = label;
+
+ if (label === "" || !containsNonASCII(label)) {
+ error = true;
+ }
+
+ transitionalProcessingForThisLabel = false;
+ }
+
+ // No need to validate if we already know there is an error.
+ if (error) {
+ continue;
+ }
+ const validation = validateLabel(label, {
+ ...options,
+ transitionalProcessing: transitionalProcessingForThisLabel,
+ isBidi
+ });
+ if (!validation) {
+ error = true;
+ }
+ }
+
+ return {
+ string: labels.join("."),
+ error
+ };
+}
+
+function toASCII(domainName, {
+ checkHyphens = false,
+ checkBidi = false,
+ checkJoiners = false,
+ useSTD3ASCIIRules = false,
+ verifyDNSLength = false,
+ transitionalProcessing = false,
+ ignoreInvalidPunycode = false
+} = {}) {
+ const result = processing(domainName, {
+ checkHyphens,
+ checkBidi,
+ checkJoiners,
+ useSTD3ASCIIRules,
+ transitionalProcessing,
+ ignoreInvalidPunycode
+ });
+ let labels = result.string.split(".");
+ labels = labels.map(l => {
+ if (containsNonASCII(l)) {
+ try {
+ return `xn--${punycode.encode(l)}`;
+ } catch {
+ result.error = true;
+ }
+ }
+ return l;
+ });
+
+ if (verifyDNSLength) {
+ const total = labels.join(".").length;
+ if (total > 253 || total === 0) {
+ result.error = true;
+ }
+
+ for (let i = 0; i < labels.length; ++i) {
+ if (labels[i].length > 63 || labels[i].length === 0) {
+ result.error = true;
+ break;
+ }
+ }
+ }
+
+ if (result.error) {
+ return null;
+ }
+ return labels.join(".");
+}
+
+function toUnicode(domainName, {
+ checkHyphens = false,
+ checkBidi = false,
+ checkJoiners = false,
+ useSTD3ASCIIRules = false,
+ transitionalProcessing = false,
+ ignoreInvalidPunycode = false
+} = {}) {
+ const result = processing(domainName, {
+ checkHyphens,
+ checkBidi,
+ checkJoiners,
+ useSTD3ASCIIRules,
+ transitionalProcessing,
+ ignoreInvalidPunycode
+ });
+
+ return {
+ domain: result.string,
+ error: result.error
+ };
+}
+
+module.exports = {
+ toASCII,
+ toUnicode
+};