'use strict'; // created 2025-09-14T17:56:24.099Z // compressed base64-encoded blob for include-ens data // source: https://github.com/adraffy/ens-normalize.js/blob/main/src/make.js // see: https://github.com/adraffy/ens-normalize.js#security // SHA-256: 92cbf3a1af3c3c0a91aee0dc542072775f4ebbbc526a84189a12da2d56f5accd var COMPRESSED$1 = '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'; const FENCED = new Map([[8217,"apostrophe"],[8260,"fraction slash"],[12539,"middle dot"]]); const NSM_MAX = 4; function decode_arithmetic(bytes) { let pos = 0; function u16() { return (bytes[pos++] << 8) | bytes[pos++]; } // decode the frequency table let symbol_count = u16(); let total = 1; let acc = [0, 1]; // first symbol has frequency 1 for (let i = 1; i < symbol_count; i++) { acc.push(total += u16()); } // skip the sized-payload that the last 3 symbols index into let skip = u16(); let pos_payload = pos; pos += skip; let read_width = 0; let read_buffer = 0; function read_bit() { if (read_width == 0) { // this will read beyond end of buffer // but (undefined|0) => zero pad read_buffer = (read_buffer << 8) | bytes[pos++]; read_width = 8; } return (read_buffer >> --read_width) & 1; } const N = 31; const FULL = 2**N; const HALF = FULL >>> 1; const QRTR = HALF >> 1; const MASK = FULL - 1; // fill register let register = 0; for (let i = 0; i < N; i++) register = (register << 1) | read_bit(); let symbols = []; let low = 0; let range = FULL; // treat like a float while (true) { let value = Math.floor((((register - low + 1) * total) - 1) / range); let start = 0; let end = symbol_count; while (end - start > 1) { // binary search let mid = (start + end) >>> 1; if (value < acc[mid]) { end = mid; } else { start = mid; } } if (start == 0) break; // first symbol is end mark symbols.push(start); let a = low + Math.floor(range * acc[start] / total); let b = low + Math.floor(range * acc[start+1] / total) - 1; while (((a ^ b) & HALF) == 0) { register = (register << 1) & MASK | read_bit(); a = (a << 1) & MASK; b = (b << 1) & MASK | 1; } while (a & ~b & QRTR) { register = (register & HALF) | ((register << 1) & (MASK >>> 1)) | read_bit(); a = (a << 1) ^ HALF; b = ((b ^ HALF) << 1) | HALF | 1; } low = a; range = 1 + b - a; } let offset = symbol_count - 4; return symbols.map(x => { // index into payload switch (x - offset) { case 3: return offset + 0x10100 + ((bytes[pos_payload++] << 16) | (bytes[pos_payload++] << 8) | bytes[pos_payload++]); case 2: return offset + 0x100 + ((bytes[pos_payload++] << 8) | bytes[pos_payload++]); case 1: return offset + bytes[pos_payload++]; default: return x - 1; } }); } // returns an iterator which returns the next symbol function read_payload(v) { let pos = 0; return () => v[pos++]; } function read_compressed_payload(s) { return read_payload(decode_arithmetic(unsafe_atob(s))); } // unsafe in the sense: // expected well-formed Base64 w/o padding // 20220922: added for https://github.com/adraffy/ens-normalize.js/issues/4 function unsafe_atob(s) { let lookup = []; [...'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'].forEach((c, i) => lookup[c.charCodeAt(0)] = i); let n = s.length; let ret = new Uint8Array((6 * n) >> 3); for (let i = 0, pos = 0, width = 0, carry = 0; i < n; i++) { carry = (carry << 6) | lookup[s.charCodeAt(i)]; width += 6; if (width >= 8) { ret[pos++] = (carry >> (width -= 8)); } } return ret; } // eg. [0,1,2,3...] => [0,-1,1,-2,...] function signed(i) { return (i & 1) ? (~i >> 1) : (i >> 1); } function read_deltas(n, next) { let v = Array(n); for (let i = 0, x = 0; i < n; i++) v[i] = x += signed(next()); return v; } // [123][5] => [0 3] [1 1] [0 0] function read_sorted(next, prev = 0) { let ret = []; while (true) { let x = next(); let n = next(); if (!n) break; prev += x; for (let i = 0; i < n; i++) { ret.push(prev + i); } prev += n + 1; } return ret; } function read_sorted_arrays(next) { return read_array_while(() => { let v = read_sorted(next); if (v.length) return v; }); } // returns map of x => ys function read_mapped(next) { let ret = []; while (true) { let w = next(); if (w == 0) break; ret.push(read_linear_table(w, next)); } while (true) { let w = next() - 1; if (w < 0) break; ret.push(read_replacement_table(w, next)); } return ret.flat(); } // read until next is falsy // return array of read values function read_array_while(next) { let v = []; while (true) { let x = next(v.length); if (!x) break; v.push(x); } return v; } // read w columns of length n // return as n rows of length w function read_transposed(n, w, next) { let m = Array(n).fill().map(() => []); for (let i = 0; i < w; i++) { read_deltas(n, next).forEach((x, j) => m[j].push(x)); } return m; } // returns [[x, ys], [x+dx, ys+dy], [x+2*dx, ys+2*dy], ...] // where dx/dy = steps, n = run size, w = length of y function read_linear_table(w, next) { let dx = 1 + next(); let dy = next(); let vN = read_array_while(next); let m = read_transposed(vN.length, 1+w, next); return m.flatMap((v, i) => { let [x, ...ys] = v; return Array(vN[i]).fill().map((_, j) => { let j_dy = j * dy; return [x + j * dx, ys.map(y => y + j_dy)]; }); }); } // return [[x, ys...], ...] // where w = length of y function read_replacement_table(w, next) { let n = 1 + next(); let m = read_transposed(n, 1+w, next); return m.map(v => [v[0], v.slice(1)]); } function read_trie(next) { let ret = []; let sorted = read_sorted(next); expand(decode([]), []); return ret; // not sorted function decode(Q) { // characters that lead into this node let S = next(); // state: valid, save, check let B = read_array_while(() => { // buckets leading to new nodes let cps = read_sorted(next).map(i => sorted[i]); if (cps.length) return decode(cps); }); return {S, B, Q}; } function expand({S, B}, cps, saved) { if (S & 4 && saved === cps[cps.length-1]) return; if (S & 2) saved = cps[cps.length-1]; if (S & 1) ret.push(cps); for (let br of B) { for (let cp of br.Q) { expand(br, [...cps, cp], saved); } } } } function hex_cp(cp) { return cp.toString(16).toUpperCase().padStart(2, '0'); } function quote_cp(cp) { return `{${hex_cp(cp)}}`; // raffy convention: like "\u{X}" w/o the "\u" } /* export function explode_cp(s) { return [...s].map(c => c.codePointAt(0)); } */ function explode_cp(s) { // this is about 2x faster let cps = []; for (let pos = 0, len = s.length; pos < len; ) { let cp = s.codePointAt(pos); pos += cp < 0x10000 ? 1 : 2; cps.push(cp); } return cps; } function str_from_cps(cps) { const chunk = 4096; let len = cps.length; if (len < chunk) return String.fromCodePoint(...cps); let buf = []; for (let i = 0; i < len; ) { buf.push(String.fromCodePoint(...cps.slice(i, i += chunk))); } return buf.join(''); } function compare_arrays(a, b) { let n = a.length; let c = n - b.length; for (let i = 0; c == 0 && i < n; i++) c = a[i] - b[i]; return c; } function array_replace(v, a, b) { let prev = 0; while (true) { let next = v.indexOf(a, prev); if (next < 0) break; v[next] = b; prev = next + 1; } } // created 2025-09-14T17:56:24.099Z // compressed base64-encoded blob for include-nf data // source: https://github.com/adraffy/ens-normalize.js/blob/main/src/make.js // see: https://github.com/adraffy/ens-normalize.js#security // SHA-256: 9ef43cc7215aa7a53e4ed9afa3b4f2f8ce00a2c708b9eb96aa409ae6fa3fb6af var COMPRESSED = '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'; // https://unicode.org/reports/tr15/ // for reference implementation // see: /derive/nf.js // algorithmic hangul // https://www.unicode.org/versions/Unicode15.0.0/ch03.pdf (page 144) const S0 = 0xAC00; const L0 = 0x1100; const V0 = 0x1161; const T0 = 0x11A7; const L_COUNT = 19; const V_COUNT = 21; const T_COUNT = 28; const N_COUNT = V_COUNT * T_COUNT; const S_COUNT = L_COUNT * N_COUNT; const S1 = S0 + S_COUNT; const L1 = L0 + L_COUNT; const V1 = V0 + V_COUNT; const T1 = T0 + T_COUNT; function unpack_cc(packed) { return (packed >> 24) & 0xFF; } function unpack_cp(packed) { return packed & 0xFFFFFF; } let SHIFTED_RANK, EXCLUSIONS, DECOMP, RECOMP; // export function nf_deinit() { // if (!SHIFTED_RANK) return; // SHIFTED_RANK = EXCLUSIONS = DECOMP = RECOMP = undefined; // } function init$1() { //console.time('nf'); let r = read_compressed_payload(COMPRESSED); SHIFTED_RANK = new Map(read_sorted_arrays(r).flatMap((v, i) => v.map(x => [x, (i+1) << 24]))); // pre-shifted EXCLUSIONS = new Set(read_sorted(r)); DECOMP = new Map(); RECOMP = new Map(); for (let [cp, cps] of read_mapped(r)) { if (!EXCLUSIONS.has(cp) && cps.length == 2) { let [a, b] = cps; let bucket = RECOMP.get(a); if (!bucket) { bucket = new Map(); RECOMP.set(a, bucket); } bucket.set(b, cp); } DECOMP.set(cp, cps.reverse()); // stored reversed } //console.timeEnd('nf'); // 20230905: 11ms } function is_hangul(cp) { return cp >= S0 && cp < S1; } function compose_pair(a, b) { if (a >= L0 && a < L1 && b >= V0 && b < V1) { return S0 + (a - L0) * N_COUNT + (b - V0) * T_COUNT; } else if (is_hangul(a) && b > T0 && b < T1 && (a - S0) % T_COUNT == 0) { return a + (b - T0); } else { let recomp = RECOMP.get(a); if (recomp) { recomp = recomp.get(b); if (recomp) { return recomp; } } return -1; } } function decomposed(cps) { if (!SHIFTED_RANK) init$1(); let ret = []; let buf = []; let check_order = false; function add(cp) { let cc = SHIFTED_RANK.get(cp); if (cc) { check_order = true; cp |= cc; } ret.push(cp); } for (let cp of cps) { while (true) { if (cp < 0x80) { ret.push(cp); } else if (is_hangul(cp)) { let s_index = cp - S0; let l_index = s_index / N_COUNT | 0; let v_index = (s_index % N_COUNT) / T_COUNT | 0; let t_index = s_index % T_COUNT; add(L0 + l_index); add(V0 + v_index); if (t_index > 0) add(T0 + t_index); } else { let mapped = DECOMP.get(cp); if (mapped) { buf.push(...mapped); } else { add(cp); } } if (!buf.length) break; cp = buf.pop(); } } if (check_order && ret.length > 1) { let prev_cc = unpack_cc(ret[0]); for (let i = 1; i < ret.length; i++) { let cc = unpack_cc(ret[i]); if (cc == 0 || prev_cc <= cc) { prev_cc = cc; continue; } let j = i-1; while (true) { let tmp = ret[j+1]; ret[j+1] = ret[j]; ret[j] = tmp; if (!j) break; prev_cc = unpack_cc(ret[--j]); if (prev_cc <= cc) break; } prev_cc = unpack_cc(ret[i]); } } return ret; } function composed_from_decomposed(v) { let ret = []; let stack = []; let prev_cp = -1; let prev_cc = 0; for (let packed of v) { let cc = unpack_cc(packed); let cp = unpack_cp(packed); if (prev_cp == -1) { if (cc == 0) { prev_cp = cp; } else { ret.push(cp); } } else if (prev_cc > 0 && prev_cc >= cc) { if (cc == 0) { ret.push(prev_cp, ...stack); stack.length = 0; prev_cp = cp; } else { stack.push(cp); } prev_cc = cc; } else { let composed = compose_pair(prev_cp, cp); if (composed >= 0) { prev_cp = composed; } else if (prev_cc == 0 && cc == 0) { ret.push(prev_cp); prev_cp = cp; } else { stack.push(cp); prev_cc = cc; } } } if (prev_cp >= 0) { ret.push(prev_cp, ...stack); } return ret; } // note: cps can be iterable function nfd(cps) { return decomposed(cps).map(unpack_cp); } function nfc(cps) { return composed_from_decomposed(decomposed(cps)); } const HYPHEN = 0x2D; const STOP = 0x2E; const STOP_CH = '.'; const FE0F = 0xFE0F; const UNIQUE_PH = 1; // 20230913: replace [...v] with Array_from(v) to avoid large spreads const Array_from = x => Array.from(x); // Array.from.bind(Array); function group_has_cp(g, cp) { // 20230913: keep primary and secondary distinct instead of creating valid union return g.P.has(cp) || g.Q.has(cp); } class Emoji extends Array { get is_emoji() { return true; } // free tagging system } let MAPPED, IGNORED, CM, NSM, ESCAPE, NFC_CHECK, GROUPS, WHOLE_VALID, WHOLE_MAP, VALID, EMOJI_LIST, EMOJI_ROOT; // export function ens_deinit() { // nf_deinit(); // if (!MAPPED) return; // MAPPED = IGNORED = CM = NSM = ESCAPE = NFC_CHECK = GROUPS = WHOLE_VALID = WHOLE_MAP = VALID = EMOJI_LIST = EMOJI_ROOT = undefined; // } function init() { if (MAPPED) return; let r = read_compressed_payload(COMPRESSED$1); const read_sorted_array = () => read_sorted(r); const read_sorted_set = () => new Set(read_sorted_array()); const set_add_many = (set, v) => v.forEach(x => set.add(x)); MAPPED = new Map(read_mapped(r)); IGNORED = read_sorted_set(); // ignored characters are not valid, so just read raw codepoints /* // direct include from payload is smaller than the decompression code const FENCED = new Map(read_array_while(() => { let cp = r(); if (cp) return [cp, read_str(r())]; })); */ // 20230217: we still need all CM for proper error formatting // but norm only needs NSM subset that are potentially-valid CM = read_sorted_array(); NSM = new Set(read_sorted_array().map(i => CM[i])); CM = new Set(CM); ESCAPE = read_sorted_set(); // characters that should not be printed NFC_CHECK = read_sorted_set(); // only needed to illustrate ens_tokenize() transformations let chunks = read_sorted_arrays(r); let unrestricted = r(); //const read_chunked = () => new Set(read_sorted_array().flatMap(i => chunks[i]).concat(read_sorted_array())); const read_chunked = () => { // 20230921: build set in parts, 2x faster let set = new Set(); read_sorted_array().forEach(i => set_add_many(set, chunks[i])); set_add_many(set, read_sorted_array()); return set; }; GROUPS = read_array_while(i => { // minifier property mangling seems unsafe // so these are manually renamed to single chars let N = read_array_while(r).map(x => x+0x60); if (N.length) { let R = i >= unrestricted; // unrestricted then restricted N[0] -= 32; // capitalize N = str_from_cps(N); if (R) N=`Restricted[${N}]`; let P = read_chunked(); // primary let Q = read_chunked(); // secondary let M = !r(); // not-whitelisted, check for NSM // *** this code currently isn't needed *** /* let V = [...P, ...Q].sort((a, b) => a-b); // derive: sorted valid let M = r()-1; // number of combining mark if (M < 0) { // whitelisted M = new Map(read_array_while(() => { let i = r(); if (i) return [V[i-1], read_array_while(() => { let v = read_array_while(r); if (v.length) return v.map(x => x-1); })]; })); }*/ return {N, P, Q, M, R}; } }); // decode compressed wholes WHOLE_VALID = read_sorted_set(); WHOLE_MAP = new Map(); let wholes = read_sorted_array().concat(Array_from(WHOLE_VALID)).sort((a, b) => a-b); // must be sorted wholes.forEach((cp, i) => { let d = r(); let w = wholes[i] = d ? wholes[i-d] : {V: [], M: new Map()}; w.V.push(cp); // add to member set if (!WHOLE_VALID.has(cp)) { WHOLE_MAP.set(cp, w); // register with whole map } }); // compute confusable-extent complements // usage: WHOLE_MAP.get(cp).M.get(cp) = complement set for (let {V, M} of new Set(WHOLE_MAP.values())) { // connect all groups that have each whole character let recs = []; for (let cp of V) { let gs = GROUPS.filter(g => group_has_cp(g, cp)); let rec = recs.find(({G}) => gs.some(g => G.has(g))); if (!rec) { rec = {G: new Set(), V: []}; recs.push(rec); } rec.V.push(cp); set_add_many(rec.G, gs); } // per character cache groups which are not a member of the extent let union = recs.flatMap(x => Array_from(x.G)); // all of the groups used by this whole for (let {G, V} of recs) { let complement = new Set(union.filter(g => !G.has(g))); // groups not covered by the extent for (let cp of V) { M.set(cp, complement); // this is the same reference } } } // compute valid set // 20230924: VALID was union but can be re-used VALID = new Set(); // exists in 1+ groups let multi = new Set(); // exists in 2+ groups const add_to_union = cp => VALID.has(cp) ? multi.add(cp) : VALID.add(cp); for (let g of GROUPS) { for (let cp of g.P) add_to_union(cp); for (let cp of g.Q) add_to_union(cp); } // dual purpose WHOLE_MAP: return placeholder if unique non-confusable for (let cp of VALID) { if (!WHOLE_MAP.has(cp) && !multi.has(cp)) { WHOLE_MAP.set(cp, UNIQUE_PH); } } // add all decomposed parts // see derive: "Valid is Closed (via Brute-force)" set_add_many(VALID, nfd(VALID)); // decode emoji // 20230719: emoji are now fully-expanded to avoid quirk logic EMOJI_LIST = read_trie(r).map(v => Emoji.from(v)).sort(compare_arrays); EMOJI_ROOT = new Map(); // this has approx 7K nodes (2+ per emoji) for (let cps of EMOJI_LIST) { // 20230719: change to *slightly* stricter algorithm which disallows // insertion of misplaced FE0F in emoji sequences (matching ENSIP-15) // example: beautified [A B] (eg. flag emoji) // before: allow: [A FE0F B], error: [A FE0F FE0F B] // after: error: both // note: this code now matches ENSNormalize.{cs,java} logic let prev = [EMOJI_ROOT]; for (let cp of cps) { let next = prev.map(node => { let child = node.get(cp); if (!child) { // should this be object? // (most have 1-2 items, few have many) // 20230719: no, v8 default map is 4? child = new Map(); node.set(cp, child); } return child; }); if (cp === FE0F) { prev.push(...next); // less than 20 elements } else { prev = next; } } for (let x of prev) { x.V = cps; } } } // if escaped: {HEX} // else: "x" {HEX} function quoted_cp(cp) { return (should_escape(cp) ? '' : `${bidi_qq(safe_str_from_cps([cp]))} `) + quote_cp(cp); } // 20230211: some messages can be mixed-directional and result in spillover // use 200E after a quoted string to force the remainder of a string from // acquring the direction of the quote // https://www.w3.org/International/questions/qa-bidi-unicode-controls#exceptions function bidi_qq(s) { return `"${s}"\u200E`; // strong LTR } function check_label_extension(cps) { if (cps.length >= 4 && cps[2] == HYPHEN && cps[3] == HYPHEN) { throw new Error(`invalid label extension: "${str_from_cps(cps.slice(0, 4))}"`); // this can only be ascii so cant be bidi } } function check_leading_underscore(cps) { const UNDERSCORE = 0x5F; for (let i = cps.lastIndexOf(UNDERSCORE); i > 0; ) { if (cps[--i] !== UNDERSCORE) { throw new Error('underscore allowed only at start'); } } } // check that a fenced cp is not leading, trailing, or touching another fenced cp function check_fenced(cps) { let cp = cps[0]; let prev = FENCED.get(cp); if (prev) throw error_placement(`leading ${prev}`); let n = cps.length; let last = -1; // prevents trailing from throwing for (let i = 1; i < n; i++) { cp = cps[i]; let match = FENCED.get(cp); if (match) { // since cps[0] isn't fenced, cps[1] cannot throw if (last == i) throw error_placement(`${prev} + ${match}`); last = i + 1; prev = match; } } if (last == n) throw error_placement(`trailing ${prev}`); } // create a safe to print string // invisibles are escaped // leading cm uses placeholder // if cps exceed max, middle truncate with ellipsis // quoter(cp) => string, eg. 3000 => "{3000}" // note: in html, you'd call this function then replace [<>&] with entities function safe_str_from_cps(cps, max = Infinity, quoter = quote_cp) { //if (Number.isInteger(cps)) cps = [cps]; //if (!Array.isArray(cps)) throw new TypeError(`expected codepoints`); let buf = []; if (is_combining_mark(cps[0])) buf.push('◌'); if (cps.length > max) { max >>= 1; cps = [...cps.slice(0, max), 0x2026, ...cps.slice(-max)]; } let prev = 0; let n = cps.length; for (let i = 0; i < n; i++) { let cp = cps[i]; if (should_escape(cp)) { buf.push(str_from_cps(cps.slice(prev, i))); buf.push(quoter(cp)); prev = i + 1; } } buf.push(str_from_cps(cps.slice(prev, n))); return buf.join(''); } // note: set(s) cannot be exposed because they can be modified // note: Object.freeze() doesn't work function is_combining_mark(cp, only_nsm) { // 20240127: add extra argument init(); return only_nsm ? NSM.has(cp) : CM.has(cp); } function should_escape(cp) { init(); return ESCAPE.has(cp); } // return all supported emoji as fully-qualified emoji // ordered by length then lexicographic function ens_emoji() { init(); return EMOJI_LIST.map(x => x.slice()); // emoji are exposed so copy } function ens_normalize_fragment(frag, decompose) { init(); let nf = decompose ? nfd : nfc; return frag.split(STOP_CH).map(label => str_from_cps(tokens_from_str(explode_cp(label), nf, filter_fe0f).flat())).join(STOP_CH); } function ens_normalize(name) { return flatten(split(name, nfc, filter_fe0f)); } function ens_beautify(name) { let labels = split(name, nfc, x => x); // emoji not exposed for (let {type, output, error} of labels) { if (error) break; // flatten will throw // replace leading/trailing hyphen // 20230121: consider beautifing all or leading/trailing hyphen to unicode variant // not exactly the same in every font, but very similar: "-" vs "‐" /* const UNICODE_HYPHEN = 0x2010; // maybe this should replace all for visual consistancy? // `node tools/reg-count.js regex ^-\{2,\}` => 592 //for (let i = 0; i < output.length; i++) if (output[i] == 0x2D) output[i] = 0x2010; if (output[0] == HYPHEN) output[0] = UNICODE_HYPHEN; let end = output.length-1; if (output[end] == HYPHEN) output[end] = UNICODE_HYPHEN; */ // 20230123: WHATWG URL uses "CheckHyphens" false // https://url.spec.whatwg.org/#idna // update ethereum symbol // ξ => Ξ if not greek if (type !== 'Greek') array_replace(output, 0x3BE, 0x39E); // 20221213: fixes bidi subdomain issue, but breaks invariant (200E is disallowed) // could be fixed with special case for: 2D (.) + 200E (LTR) // https://discuss.ens.domains/t/bidi-label-ordering-spoof/15824 //output.splice(0, 0, 0x200E); } return flatten(labels); } function ens_split(name, preserve_emoji) { return split(name, nfc, preserve_emoji ? x => x.slice() : filter_fe0f); // emoji are exposed so copy } function split(name, nf, ef) { if (!name) return []; // 20230719: empty name allowance init(); let offset = 0; // https://unicode.org/reports/tr46/#Validity_Criteria // 4.) "The label must not contain a U+002E ( . ) FULL STOP." return name.split(STOP_CH).map(label => { let input = explode_cp(label); let info = { input, offset, // codepoint, not substring! }; offset += input.length + 1; // + stop try { // 1.) "The label must be in Unicode Normalization Form NFC" let tokens = info.tokens = tokens_from_str(input, nf, ef); let token_count = tokens.length; let type; if (!token_count) { // the label was effectively empty (could of had ignored characters) //norm = []; //type = 'None'; // use this instead of next match, "ASCII" // 20230120: change to strict // https://discuss.ens.domains/t/ens-name-normalization-2nd/14564/59 throw new Error(`empty label`); } let norm = info.output = tokens.flat(); check_leading_underscore(norm); let emoji = info.emoji = token_count > 1 || tokens[0].is_emoji; // same as: tokens.some(x => x.is_emoji); if (!emoji && norm.every(cp => cp < 0x80)) { // special case for ascii // 20230123: matches matches WHATWG, see note 3.3 check_label_extension(norm); // only needed for ascii // cant have fenced // cant have cm // cant have wholes // see derive: "Fastpath ASCII" type = 'ASCII'; } else { let chars = tokens.flatMap(x => x.is_emoji ? [] : x); // all of the nfc tokens concat together if (!chars.length) { // theres no text, just emoji type = 'Emoji'; } else { // 5.) "The label must not begin with a combining mark, that is: General_Category=Mark." if (CM.has(norm[0])) throw error_placement('leading combining mark'); for (let i = 1; i < token_count; i++) { // we've already checked the first token let cps = tokens[i]; if (!cps.is_emoji && CM.has(cps[0])) { // every text token has emoji neighbors, eg. EtEEEtEt... // bidi_qq() not needed since emoji is LTR and cps is a CM throw error_placement(`emoji + combining mark: "${str_from_cps(tokens[i-1])} + ${safe_str_from_cps([cps[0]])}"`); } } check_fenced(norm); let unique = Array_from(new Set(chars)); let [g] = determine_group(unique); // take the first match // see derive: "Matching Groups have Same CM Style" // alternative: could form a hybrid type: Latin/Japanese/... check_group(g, chars); // need text in order check_whole(g, unique); // only need unique text (order would be required for multiple-char confusables) type = g.N; // 20230121: consider exposing restricted flag // it's simpler to just check for 'Restricted' // or even better: type.endsWith(']') //if (g.R) info.restricted = true; } } info.type = type; } catch (err) { info.error = err; // use full error object } return info; }); } function check_whole(group, unique) { let maker; let shared = []; for (let cp of unique) { let whole = WHOLE_MAP.get(cp); if (whole === UNIQUE_PH) return; // unique, non-confusable if (whole) { let set = whole.M.get(cp); // groups which have a character that look-like this character maker = maker ? maker.filter(g => set.has(g)) : Array_from(set); if (!maker.length) return; // confusable intersection is empty } else { shared.push(cp); } } if (maker) { // we have 1+ confusable // check if any of the remaining groups // contain the shared characters too for (let g of maker) { if (shared.every(cp => group_has_cp(g, cp))) { throw new Error(`whole-script confusable: ${group.N}/${g.N}`); } } } } // assumption: unique.size > 0 // returns list of matching groups function determine_group(unique) { let groups = GROUPS; for (let cp of unique) { // note: we need to dodge CM that are whitelisted // but that code isn't currently necessary let gs = groups.filter(g => group_has_cp(g, cp)); if (!gs.length) { if (!GROUPS.some(g => group_has_cp(g, cp))) { // the character was composed of valid parts // but it's NFC form is invalid // 20230716: change to more exact statement, see: ENSNormalize.{cs,java} // note: this doesn't have to be a composition // 20230720: change to full check throw error_disallowed(cp); // this should be rare } else { // there is no group that contains all these characters // throw using the highest priority group that matched // https://www.unicode.org/reports/tr39/#mixed_script_confusables throw error_group_member(groups[0], cp); } } groups = gs; if (gs.length == 1) break; // there is only one group left } // there are at least 1 group(s) with all of these characters return groups; } // throw on first error function flatten(split) { return split.map(({input, error, output}) => { if (error) { // don't print label again if just a single label let msg = error.message; // bidi_qq() only necessary if msg is digits throw new Error(split.length == 1 ? msg : `Invalid label ${bidi_qq(safe_str_from_cps(input, 63))}: ${msg}`); } return str_from_cps(output); }).join(STOP_CH); } function error_disallowed(cp) { // TODO: add cp to error? return new Error(`disallowed character: ${quoted_cp(cp)}`); } function error_group_member(g, cp) { let quoted = quoted_cp(cp); let gg = GROUPS.find(g => g.P.has(cp)); // only check primary if (gg) { quoted = `${gg.N} ${quoted}`; } return new Error(`illegal mixture: ${g.N} + ${quoted}`); } function error_placement(where) { return new Error(`illegal placement: ${where}`); } // assumption: cps.length > 0 // assumption: cps[0] isn't a CM // assumption: the previous character isn't an emoji function check_group(g, cps) { for (let cp of cps) { if (!group_has_cp(g, cp)) { // for whitelisted scripts, this will throw illegal mixture on invalid cm, eg. "e{300}{300}" // at the moment, it's unnecessary to introduce an extra error type // until there exists a whitelisted multi-character // eg. if (M < 0 && is_combining_mark(cp)) { ... } // there are 3 cases: // 1. illegal cm for wrong group => mixture error // 2. illegal cm for same group => cm error // requires set of whitelist cm per group: // eg. new Set([...g.P, ...g.Q].flatMap(nfc).filter(cp => CM.has(cp))) // 3. wrong group => mixture error throw error_group_member(g, cp); } } //if (M >= 0) { // we have a known fixed cm count if (g.M) { // we need to check for NSM let decomposed = nfd(cps); for (let i = 1, e = decomposed.length; i < e; i++) { // see: assumption // 20230210: bugfix: using cps instead of decomposed h/t Carbon225 /* if (CM.has(decomposed[i])) { let j = i + 1; while (j < e && CM.has(decomposed[j])) j++; if (j - i > M) { throw new Error(`too many combining marks: ${g.N} ${bidi_qq(str_from_cps(decomposed.slice(i-1, j)))} (${j-i}/${M})`); } i = j; } */ // 20230217: switch to NSM counting // https://www.unicode.org/reports/tr39/#Optional_Detection if (NSM.has(decomposed[i])) { let j = i + 1; for (let cp; j < e && NSM.has(cp = decomposed[j]); j++) { // a. Forbid sequences of the same nonspacing mark. for (let k = i; k < j; k++) { // O(n^2) but n < 100 if (decomposed[k] == cp) { throw new Error(`duplicate non-spacing marks: ${quoted_cp(cp)}`); } } } // parse to end so we have full nsm count // b. Forbid sequences of more than 4 nonspacing marks (gc=Mn or gc=Me). if (j - i > NSM_MAX) { // note: this slice starts with a base char or spacing-mark cm throw new Error(`excessive non-spacing marks: ${bidi_qq(safe_str_from_cps(decomposed.slice(i-1, j)))} (${j-i}/${NSM_MAX})`); } i = j; } } } // *** this code currently isn't needed *** /* let cm_whitelist = M instanceof Map; for (let i = 0, e = cps.length; i < e; ) { let cp = cps[i++]; let seqs = cm_whitelist && M.get(cp); if (seqs) { // list of codepoints that can follow // if this exists, this will always be 1+ let j = i; while (j < e && CM.has(cps[j])) j++; let cms = cps.slice(i, j); let match = seqs.find(seq => !compare_arrays(seq, cms)); if (!match) throw new Error(`disallowed combining mark sequence: "${safe_str_from_cps([cp, ...cms])}"`); i = j; } else if (!V.has(cp)) { // https://www.unicode.org/reports/tr39/#mixed_script_confusables let quoted = quoted_cp(cp); for (let cp of cps) { let u = UNIQUE.get(cp); if (u && u !== g) { // if both scripts are restricted this error is confusing // because we don't differentiate RestrictedA from RestrictedB if (!u.R) quoted = `${quoted} is ${u.N}`; break; } } throw new Error(`disallowed ${g.N} character: ${quoted}`); //throw new Error(`disallowed character: ${quoted} (expected ${g.N})`); //throw new Error(`${g.N} does not allow: ${quoted}`); } } if (!cm_whitelist) { let decomposed = nfd(cps); for (let i = 1, e = decomposed.length; i < e; i++) { // we know it can't be cm leading if (CM.has(decomposed[i])) { let j = i + 1; while (j < e && CM.has(decomposed[j])) j++; if (j - i > M) { throw new Error(`too many combining marks: "${str_from_cps(decomposed.slice(i-1, j))}" (${j-i}/${M})`); } i = j; } } } */ } // given a list of codepoints // returns a list of lists, where emoji are a fully-qualified (as Array subclass) // eg. explode_cp("abc💩d") => [[61, 62, 63], Emoji[1F4A9, FE0F], [64]] // 20230818: rename for 'process' name collision h/t Javarome // https://github.com/adraffy/ens-normalize.js/issues/23 function tokens_from_str(input, nf, ef) { let ret = []; let chars = []; input = input.slice().reverse(); // flip so we can pop while (input.length) { let emoji = consume_emoji_reversed(input); if (emoji) { if (chars.length) { ret.push(nf(chars)); chars = []; } ret.push(ef(emoji)); } else { let cp = input.pop(); if (VALID.has(cp)) { chars.push(cp); } else { let cps = MAPPED.get(cp); if (cps) { chars.push(...cps); // less than 10 elements } else if (!IGNORED.has(cp)) { // 20230912: unicode 15.1 changed the order of processing such that // disallowed parts are only rejected after NFC // https://unicode.org/reports/tr46/#Validity_Criteria // this doesn't impact normalization as of today // technically, this error can be removed as the group logic will apply similar logic // however the error type might be less clear throw error_disallowed(cp); } } } } if (chars.length) { ret.push(nf(chars)); } return ret; } function filter_fe0f(cps) { return cps.filter(cp => cp != FE0F); } // given array of codepoints // returns the longest valid emoji sequence (or undefined if no match) // *MUTATES* the supplied array // disallows interleaved ignored characters // fills (optional) eaten array with matched codepoints function consume_emoji_reversed(cps, eaten) { let node = EMOJI_ROOT; let emoji; let pos = cps.length; while (pos) { node = node.get(cps[--pos]); if (!node) break; let {V} = node; if (V) { // this is a valid emoji (so far) emoji = V; if (eaten) eaten.push(...cps.slice(pos).reverse()); // (optional) copy input, used for ens_tokenize() cps.length = pos; // truncate } } return emoji; } // ************************************************************ // tokenizer const TY_VALID = 'valid'; const TY_MAPPED = 'mapped'; const TY_IGNORED = 'ignored'; const TY_DISALLOWED = 'disallowed'; const TY_EMOJI = 'emoji'; const TY_NFC = 'nfc'; const TY_STOP = 'stop'; function ens_tokenize(name, { nf = true, // collapse unnormalized runs into a single token } = {}) { init(); let input = explode_cp(name).reverse(); let eaten = []; let tokens = []; while (input.length) { let emoji = consume_emoji_reversed(input, eaten); if (emoji) { tokens.push({ type: TY_EMOJI, emoji: emoji.slice(), // copy emoji input: eaten, cps: filter_fe0f(emoji) }); eaten = []; // reset buffer } else { let cp = input.pop(); if (cp == STOP) { tokens.push({type: TY_STOP, cp}); } else if (VALID.has(cp)) { tokens.push({type: TY_VALID, cps: [cp]}); } else if (IGNORED.has(cp)) { tokens.push({type: TY_IGNORED, cp}); } else { let cps = MAPPED.get(cp); if (cps) { tokens.push({type: TY_MAPPED, cp, cps: cps.slice()}); } else { tokens.push({type: TY_DISALLOWED, cp}); } } } } if (nf) { for (let i = 0, start = -1; i < tokens.length; i++) { let token = tokens[i]; if (is_valid_or_mapped(token.type)) { if (requires_check(token.cps)) { // normalization might be needed let end = i + 1; for (let pos = end; pos < tokens.length; pos++) { // find adjacent text let {type, cps} = tokens[pos]; if (is_valid_or_mapped(type)) { if (!requires_check(cps)) break; end = pos + 1; } else if (type !== TY_IGNORED) { // || type !== TY_DISALLOWED) { break; } } if (start < 0) start = i; let slice = tokens.slice(start, end); let cps0 = slice.flatMap(x => is_valid_or_mapped(x.type) ? x.cps : []); // strip junk tokens let cps = nfc(cps0); if (compare_arrays(cps, cps0)) { // bundle into an nfc token tokens.splice(start, end - start, { type: TY_NFC, input: cps0, // there are 3 states: tokens0 ==(process)=> input ==(nfc)=> tokens/cps cps, tokens0: collapse_valid_tokens(slice), tokens: ens_tokenize(str_from_cps(cps), {nf: false}) }); i = start; } else { i = end - 1; // skip to end of slice } start = -1; // reset } else { start = i; // remember last } } else if (token.type !== TY_IGNORED) { // 20221024: is this correct? start = -1; // reset } } } return collapse_valid_tokens(tokens); } function is_valid_or_mapped(type) { return type == TY_VALID || type == TY_MAPPED; } function requires_check(cps) { return cps.some(cp => NFC_CHECK.has(cp)); } function collapse_valid_tokens(tokens) { for (let i = 0; i < tokens.length; i++) { if (tokens[i].type == TY_VALID) { let j = i + 1; while (j < tokens.length && tokens[j].type == TY_VALID) j++; tokens.splice(i, j - i, {type: TY_VALID, cps: tokens.slice(i, j).flatMap(x => x.cps)}); } } return tokens; } exports.ens_beautify = ens_beautify; exports.ens_emoji = ens_emoji; exports.ens_normalize = ens_normalize; exports.ens_normalize_fragment = ens_normalize_fragment; exports.ens_split = ens_split; exports.ens_tokenize = ens_tokenize; exports.is_combining_mark = is_combining_mark; exports.nfc = nfc; exports.nfd = nfd; exports.safe_str_from_cps = safe_str_from_cps; exports.should_escape = should_escape;