index-xnf.mjs•53.3 kB
// created 2023-09-25T01:01:55.148Z
// 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: 0565ed049b9cf1614bb9e11ba7d8ac6a6fb96c893253d890f7e2b2884b9ded32
var COMPRESSED = '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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;
}
// reverse polyfill
// replace custom nf implementation with system implementation
// (saves approximately 6KB)
function nf(cps, form) {
return explode_cp(str_from_cps(cps).normalize(form));
}
function nfc(cps) {
return nf(cps, 'NFC');
}
function nfd(cps) {
return nf(cps, 'NFD');
}
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;
function init() {
if (MAPPED) return;
let r = read_compressed_payload(COMPRESSED);
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) {
init();
return 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 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;
}
}
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;
}
export { ens_beautify, ens_emoji, ens_normalize, ens_normalize_fragment, ens_split, ens_tokenize, is_combining_mark, nfc, nfd, safe_str_from_cps, should_escape };