import { BitArray, Error, List, Ok, Result, UtfCodepoint, stringBits, toBitArray, NonEmpty, CustomType, } from "./gleam.mjs"; import { CompileError as RegexCompileError, Match as RegexMatch, } from "./gleam/regex.mjs"; import { DecodeError } from "./gleam/dynamic.mjs"; import { Some, None } from "./gleam/option.mjs"; import Dict from "./dict.mjs"; const Nil = undefined; const NOT_FOUND = {}; export function identity(x) { return x; } export function parse_int(value) { if (/^[-+]?(\d+)$/.test(value)) { return new Ok(parseInt(value)); } else { return new Error(Nil); } } export function parse_float(value) { if (/^[-+]?(\d+)\.(\d+)$/.test(value)) { return new Ok(parseFloat(value)); } else { return new Error(Nil); } } export function to_string(term) { return term.toString(); } export function float_to_string(float) { const string = float.toString(); if (string.indexOf(".") >= 0) { return string; } else { return string + ".0"; } } export function int_to_base_string(int, base) { return int.toString(base).toUpperCase(); } const int_base_patterns = { 2: /[^0-1]/, 3: /[^0-2]/, 4: /[^0-3]/, 5: /[^0-4]/, 6: /[^0-5]/, 7: /[^0-6]/, 8: /[^0-7]/, 9: /[^0-8]/, 10: /[^0-9]/, 11: /[^0-9a]/, 12: /[^0-9a-b]/, 13: /[^0-9a-c]/, 14: /[^0-9a-d]/, 15: /[^0-9a-e]/, 16: /[^0-9a-f]/, 17: /[^0-9a-g]/, 18: /[^0-9a-h]/, 19: /[^0-9a-i]/, 20: /[^0-9a-j]/, 21: /[^0-9a-k]/, 22: /[^0-9a-l]/, 23: /[^0-9a-m]/, 24: /[^0-9a-n]/, 25: /[^0-9a-o]/, 26: /[^0-9a-p]/, 27: /[^0-9a-q]/, 28: /[^0-9a-r]/, 29: /[^0-9a-s]/, 30: /[^0-9a-t]/, 31: /[^0-9a-u]/, 32: /[^0-9a-v]/, 33: /[^0-9a-w]/, 34: /[^0-9a-x]/, 35: /[^0-9a-y]/, 36: /[^0-9a-z]/, }; export function int_from_base_string(string, base) { if (int_base_patterns[base].test(string.replace(/^-/, "").toLowerCase())) { return new Error(Nil); } const result = parseInt(string, base); if (isNaN(result)) { return new Error(Nil); } return new Ok(result); } export function string_replace(string, target, substitute) { if (typeof string.replaceAll !== "undefined") { return string.replaceAll(target, substitute); } // Fallback for older Node.js versions: // 1. // 2. // TODO: This fallback could be remove once Node.js 14 is EOL // aka on or after 2024-04-30 return string.replace( // $& means the whole matched string new RegExp(target.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g"), substitute ); } export function string_reverse(string) { return [...string].reverse().join(""); } export function string_length(string) { if (string === "") { return 0; } const iterator = graphemes_iterator(string); if (iterator) { let i = 0; for (const _ of iterator) { i++; } return i; } else { return string.match(/./gsu).length; } } export function graphemes(string) { return List.fromArray( Array.from(graphemes_iterator(string)).map((item) => item.segment) ); } function graphemes_iterator(string) { if (Intl && Intl.Segmenter) { return new Intl.Segmenter().segment(string)[Symbol.iterator](); } } export function pop_grapheme(string) { let first; const iterator = graphemes_iterator(string); if (iterator) { first = iterator.next().value?.segment; } else { first = string.match(/./su)?.[0]; } if (first) { return new Ok([first, string.slice(first.length)]); } else { return new Error(Nil); } } export function lowercase(string) { return string.toLowerCase(); } export function uppercase(string) { return string.toUpperCase(); } export function less_than(a, b) { return a < b; } export function add(a, b) { return a + b; } export function equal(a, b) { return a === b; } export function split(xs, pattern) { return List.fromArray(xs.split(pattern)); } export function join(xs, separator) { const iterator = xs[Symbol.iterator](); let result = iterator.next().value || ""; let current = iterator.next(); while (!current.done) { result = result + separator + current.value; current = iterator.next(); } return result; } export function concat(xs) { let result = ""; for (const x of xs) { result = result + x; } return result; } export function length(data) { return data.length; } export function crop_string(string, substring) { return string.substring(string.indexOf(substring)); } export function contains_string(haystack, needle) { return haystack.indexOf(needle) >= 0; } export function starts_with(haystack, needle) { return haystack.startsWith(needle); } export function ends_with(haystack, needle) { return haystack.endsWith(needle); } export function split_once(haystack, needle) { const index = haystack.indexOf(needle); if (index >= 0) { const before = haystack.slice(0, index); const after = haystack.slice(index + needle.length); return new Ok([before, after]); } else { return new Error(Nil); } } export function trim(string) { return string.trim(); } export function trim_left(string) { return string.trimLeft(); } export function trim_right(string) { return string.trimRight(); } export function bit_array_from_string(string) { return toBitArray([stringBits(string)]); } export function bit_array_concat(bit_arrays) { return toBitArray(bit_arrays.toArray().map((b) => b.buffer)); } export function console_log(term) { console.log(term); } export function console_error(term) { console.error(term); } export function crash(message) { throw new globalThis.Error(message); } export function bit_array_to_string(bit_array) { try { const decoder = new TextDecoder("utf-8", { fatal: true }); return new Ok(decoder.decode(bit_array.buffer)); } catch (_error) { return new Error(Nil); } } export function print(string) { if (typeof process === "object") { process.stdout.write(string); // We can write without a trailing newline } else if (typeof Deno === "object") { Deno.stdout.writeSync(new TextEncoder().encode(string)); // We can write without a trailing newline } else { console.log(string); // We're in a browser. Newlines are mandated } } export function print_error(string) { if (typeof process === "object" && process.stderr?.write) { process.stderr.write(string); // We can write without a trailing newline } else if (typeof Deno === "object") { Deno.stderr.writeSync(new TextEncoder().encode(string)); // We can write without a trailing newline } else { console.error(string); // We're in a browser. Newlines are mandated } } export function print_debug(string) { if (typeof process === "object" && process.stderr?.write) { process.stderr.write(string + "\n"); // If we're in Node.js, use `stderr` } else if (typeof Deno === "object") { Deno.stderr.writeSync(new TextEncoder().encode(string + "\n")); // If we're in Deno, use `stderr` } else { console.log(string); // Otherwise, use `console.log` (so that it doesn't look like an error) } } export function ceiling(float) { return Math.ceil(float); } export function floor(float) { return Math.floor(float); } export function round(float) { return Math.round(float); } export function truncate(float) { return Math.trunc(float); } export function power(base, exponent) { // It is checked in Gleam that: // - The base is non-negative and that the exponent is not fractional. // - The base is non-zero and the exponent is non-negative (otherwise // the result will essentially be division by zero). // It can thus be assumed that valid input is passed to the Math.pow // function and a NaN or Infinity value will not be produced. return Math.pow(base, exponent); } export function random_uniform() { const random_uniform_result = Math.random(); // With round-to-nearest-even behavior, the ranges claimed for the functions below // (excluding the one for Math.random() itself) aren't exact. // If extremely large bounds are chosen (2^53 or higher), // it's possible in extremely rare cases to calculate the usually-excluded upper bound. // Note that as numbers in JavaScript are IEEE 754 floating point numbers // See: // Because of this, we just loop 'until' we get a valid result where 0.0 <= x < 1.0: if (random_uniform_result === 1.0) { return random_uniform(); } return random_uniform_result; } export function bit_array_slice(bits, position, length) { const start = Math.min(position, position + length); const end = Math.max(position, position + length); if (start < 0 || end > bits.length) return new Error(Nil); const buffer = new Uint8Array(bits.buffer.buffer, start, Math.abs(length)); return new Ok(new BitArray(buffer)); } export function codepoint(int) { return new UtfCodepoint(int); } export function string_to_codepoint_integer_list(string) { return List.fromArray(Array.from(string).map((item) => item.codePointAt(0))); } export function utf_codepoint_list_to_string(utf_codepoint_integer_list) { return utf_codepoint_integer_list .toArray() .map((x) => String.fromCodePoint(x.value)) .join(""); } export function utf_codepoint_to_int(utf_codepoint) { return utf_codepoint.value; } export function regex_check(regex, string) { regex.lastIndex = 0; return regex.test(string); } export function compile_regex(pattern, options) { try { let flags = "gu"; if (options.case_insensitive) flags += "i"; if (options.multi_line) flags += "m"; return new Ok(new RegExp(pattern, flags)); } catch (error) { const number = (error.columnNumber || 0) | 0; return new Error(new RegexCompileError(error.message, number)); } } export function regex_scan(regex, string) { const matches = Array.from(string.matchAll(regex)).map((match) => { const content = match[0]; const submatches = []; for (let n = match.length - 1; n > 0; n--) { if (match[n]) { submatches[n - 1] = new Some(match[n]); continue; } if (submatches.length > 0) { submatches[n - 1] = new None(); } } return new RegexMatch(content, List.fromArray(submatches)); }); return List.fromArray(matches); } export function new_map() { return Dict.new(); } export function map_size(map) { return map.size; } export function map_to_list(map) { return List.fromArray(map.entries()); } export function map_remove(key, map) { return map.delete(key); } export function map_get(map, key) { const value = map.get(key, NOT_FOUND); if (value === NOT_FOUND) { return new Error(Nil); } return new Ok(value); } export function map_insert(key, value, map) { return map.set(key, value); } function unsafe_percent_decode(string) { return decodeURIComponent((string || "").replace("+", " ")); } export function percent_decode(string) { try { return new Ok(unsafe_percent_decode(string)); } catch (_error) { return new Error(Nil); } } export function percent_encode(string) { return encodeURIComponent(string); } export function parse_query(query) { try { const pairs = []; for (const section of query.split("&")) { const [key, value] = section.split("="); if (!key) continue; pairs.push([unsafe_percent_decode(key), unsafe_percent_decode(value)]); } return new Ok(List.fromArray(pairs)); } catch (_error) { return new Error(Nil); } } // From https://developer.mozilla.org/en-US/docs/Glossary/Base64#Solution_2_%E2%80%93_rewrite_the_DOMs_atob()_and_btoa()_using_JavaScript's_TypedArrays_and_UTF-8 export function encode64(bit_array) { const aBytes = bit_array.buffer; let nMod3 = 2; let sB64Enc = ""; for (let nLen = aBytes.length, nUint24 = 0, nIdx = 0; nIdx < nLen; nIdx++) { nMod3 = nIdx % 3; if (nIdx > 0 && ((nIdx * 4) / 3) % 76 === 0) { sB64Enc += "\r\n"; } nUint24 |= aBytes[nIdx] << ((16 >>> nMod3) & 24); if (nMod3 === 2 || aBytes.length - nIdx === 1) { sB64Enc += String.fromCharCode( uint6ToB64((nUint24 >>> 18) & 63), uint6ToB64((nUint24 >>> 12) & 63), uint6ToB64((nUint24 >>> 6) & 63), uint6ToB64(nUint24 & 63) ); nUint24 = 0; } } return ( sB64Enc.substr(0, sB64Enc.length - 2 + nMod3) + (nMod3 === 2 ? "" : nMod3 === 1 ? "=" : "==") ); } // From https://developer.mozilla.org/en-US/docs/Glossary/Base64#Solution_2_%E2%80%93_rewrite_the_DOMs_atob()_and_btoa()_using_JavaScript's_TypedArrays_and_UTF-8 function uint6ToB64(nUint6) { return nUint6 < 26 ? nUint6 + 65 : nUint6 < 52 ? nUint6 + 71 : nUint6 < 62 ? nUint6 - 4 : nUint6 === 62 ? 43 : nUint6 === 63 ? 47 : 65; } // From https://developer.mozilla.org/en-US/docs/Glossary/Base64#Solution_2_%E2%80%93_rewrite_the_DOMs_atob()_and_btoa()_using_JavaScript's_TypedArrays_and_UTF-8 function b64ToUint6(nChr) { return nChr > 64 && nChr < 91 ? nChr - 65 : nChr > 96 && nChr < 123 ? nChr - 71 : nChr > 47 && nChr < 58 ? nChr + 4 : nChr === 43 ? 62 : nChr === 47 ? 63 : 0; } // From https://developer.mozilla.org/en-US/docs/Glossary/Base64#Solution_2_%E2%80%93_rewrite_the_DOMs_atob()_and_btoa()_using_JavaScript's_TypedArrays_and_UTF-8 export function decode64(sBase64) { if (sBase64.match(/[^A-Za-z0-9\+\/=]/g)) return new Error(Nil); const sB64Enc = sBase64.replace(/=/g, ""); const nInLen = sB64Enc.length; const nOutLen = (nInLen * 3 + 1) >> 2; const taBytes = new Uint8Array(nOutLen); for ( let nMod3, nMod4, nUint24 = 0, nOutIdx = 0, nInIdx = 0; nInIdx < nInLen; nInIdx++ ) { nMod4 = nInIdx & 3; nUint24 |= b64ToUint6(sB64Enc.charCodeAt(nInIdx)) << (6 * (3 - nMod4)); if (nMod4 === 3 || nInLen - nInIdx === 1) { for (nMod3 = 0; nMod3 < 3 && nOutIdx < nOutLen; nMod3++, nOutIdx++) { taBytes[nOutIdx] = (nUint24 >>> ((16 >>> nMod3) & 24)) & 255; } nUint24 = 0; } } return new Ok(new BitArray(taBytes)); } export function classify_dynamic(data) { if (typeof data === "string") { return "String"; } else if (data instanceof Result) { return "Result"; } else if (data instanceof List) { return "List"; } else if (data instanceof BitArray) { return "BitArray"; } else if (data instanceof Dict) { return "Map"; } else if (Number.isInteger(data)) { return "Int"; } else if (Array.isArray(data)) { return `Tuple of ${data.length} elements`; } else if (typeof data === "number") { return "Float"; } else if (data === null) { return "Null"; } else if (data === undefined) { return "Nil"; } else { const type = typeof data; return type.charAt(0).toUpperCase() + type.slice(1); } } function decoder_error(expected, got) { return decoder_error_no_classify(expected, classify_dynamic(got)); } function decoder_error_no_classify(expected, got) { return new Error( List.fromArray([new DecodeError(expected, got, List.fromArray([]))]) ); } export function decode_string(data) { return typeof data === "string" ? new Ok(data) : decoder_error("String", data); } export function decode_int(data) { return Number.isInteger(data) ? new Ok(data) : decoder_error("Int", data); } export function decode_float(data) { return typeof data === "number" ? new Ok(data) : decoder_error("Float", data); } export function decode_bool(data) { return typeof data === "boolean" ? new Ok(data) : decoder_error("Bool", data); } export function decode_bit_array(data) { if (data instanceof BitArray) { return new Ok(data); } if (data instanceof Uint8Array) { return new Ok(new BitArray(data)); } return decoder_error("BitArray", data); } export function decode_tuple(data) { return Array.isArray(data) ? new Ok(data) : decoder_error("Tuple", data); } export function decode_tuple2(data) { return decode_tupleN(data, 2); } export function decode_tuple3(data) { return decode_tupleN(data, 3); } export function decode_tuple4(data) { return decode_tupleN(data, 4); } export function decode_tuple5(data) { return decode_tupleN(data, 5); } export function decode_tuple6(data) { return decode_tupleN(data, 6); } function decode_tupleN(data, n) { if (Array.isArray(data) && data.length == n) { return new Ok(data); } const list = decode_exact_length_list(data, n); if (list) return new Ok(list); return decoder_error(`Tuple of ${n} elements`, data); } function decode_exact_length_list(data, n) { if (!(data instanceof List)) return; const elements = []; let current = data; for (let i = 0; i < n; i++) { if (!(current instanceof NonEmpty)) break; elements.push(current.head); current = current.tail; } if (elements.length === n && !(current instanceof NonEmpty)) return elements; } export function tuple_get(data, index) { return index >= 0 && data.length > index ? new Ok(data[index]) : new Error(Nil); } export function decode_list(data) { if (Array.isArray(data)) { return new Ok(List.fromArray(data)); } return data instanceof List ? new Ok(data) : decoder_error("List", data); } export function decode_result(data) { return data instanceof Result ? new Ok(data) : decoder_error("Result", data); } export function decode_map(data) { if (data instanceof Dict) { return new Ok(Dict.fromMap(data)); } if (data == null) { return decoder_error("Map", data); } if (typeof data !== "object") { return decoder_error("Map", data); } const proto = Object.getPrototypeOf(data); if (proto === Object.prototype || proto === null) { return new Ok(Dict.fromObject(data)); } return decoder_error("Map", data); } export function decode_option(data, decoder) { if (data === null || data === undefined || data instanceof None) return new Ok(new None()); if (data instanceof Some) data = data[0]; const result = decoder(data); if (result.isOk()) { return new Ok(new Some(result[0])); } else { return result; } } export function decode_field(value, name) { const not_a_map_error = () => decoder_error("Map", value); if ( value instanceof Dict || value instanceof WeakMap || value instanceof Map ) { const entry = map_get(value, name); return new Ok(entry.isOk() ? new Some(entry[0]) : new None()); } else if (Object.getPrototypeOf(value) == Object.prototype) { return try_get_field(value, name, () => new Ok(new None())); } else { return try_get_field(value, name, not_a_map_error); } } function try_get_field(value, field, or_else) { try { return field in value ? new Ok(new Some(value[field])) : or_else(); } catch { return or_else(); } } export function byte_size(string) { return new TextEncoder().encode(string).length; } // In Javascript bitwise operations convert numbers to a sequence of 32 bits // while Erlang uses arbitrary precision. // To get around this problem and get consistent results use BigInt and then // downcast the value back to a Number value. export function bitwise_and(x, y) { return Number(BigInt(x) & BigInt(y)); } export function bitwise_not(x) { return Number(~BigInt(x)); } export function bitwise_or(x, y) { return Number(BigInt(x) | BigInt(y)); } export function bitwise_exclusive_or(x, y) { return Number(BigInt(x) ^ BigInt(y)); } export function bitwise_shift_left(x, y) { return Number(BigInt(x) << BigInt(y)); } export function bitwise_shift_right(x, y) { return Number(BigInt(x) >> BigInt(y)); } export function inspect(v) { const t = typeof v; if (v === true) return "True"; if (v === false) return "False"; if (v === null) return "//js(null)"; if (v === undefined) return "Nil"; if (t === "string") return JSON.stringify(v); if (t === "bigint" || t === "number") return v.toString(); if (Array.isArray(v)) return `#(${v.map(inspect).join(", ")})`; if (v instanceof List) return inspectList(v); if (v instanceof UtfCodepoint) return inspectUtfCodepoint(v); if (v instanceof BitArray) return inspectBitArray(v); if (v instanceof CustomType) return inspectCustomType(v); if (v instanceof Dict) return inspectDict(v); if (v instanceof Set) return `//js(Set(${[...v].map(inspect).join(", ")}))`; if (v instanceof RegExp) return `//js(${v})`; if (v instanceof Date) return `//js(Date("${v.toISOString()}"))`; if (v instanceof Function) { const args = []; for (const i of Array(v.length).keys()) args.push(String.fromCharCode(i + 97)); return `//fn(${args.join(", ")}) { ... }`; } return inspectObject(v); } function inspectDict(map) { let body = "dict.from_list(["; let first = true; map.forEach((value, key) => { if (!first) body = body + ", "; body = body + "#(" + inspect(key) + ", " + inspect(value) + ")"; first = false; }); return body + "])"; } function inspectObject(v) { const name = Object.getPrototypeOf(v)?.constructor?.name || "Object"; const props = []; for (const k of Object.keys(v)) { props.push(`${inspect(k)}: ${inspect(v[k])}`); } const body = props.length ? " " + props.join(", ") + " " : ""; const head = name === "Object" ? "" : name + " "; return `//js(${head}{${body}})`; } function inspectCustomType(record) { const props = Object.keys(record) .map((label) => { const value = inspect(record[label]); return isNaN(parseInt(label)) ? `${label}: ${value}` : value; }) .join(", "); return props ? `${record.constructor.name}(${props})` : record.constructor.name; } export function inspectList(list) { return `[${list.toArray().map(inspect).join(", ")}]`; } export function inspectBitArray(bits) { return `<<${Array.from(bits.buffer).join(", ")}>>`; } export function inspectUtfCodepoint(codepoint) { return `//utfcodepoint(${String.fromCodePoint(codepoint.value)})`; } export function base16_encode(bit_array) { let result = ""; for (const byte of bit_array.buffer) { result += byte.toString(16).padStart(2, "0").toUpperCase(); } return result; } export function base16_decode(string) { const bytes = new Uint8Array(string.length / 2); for (let i = 0; i < string.length; i += 2) { const a = parseInt(string[i], 16); const b = parseInt(string[i + 1], 16); if (isNaN(a) || isNaN(b)) return new Error(Nil); bytes[i / 2] = a * 16 + b; } return new Ok(new BitArray(bytes)); }