import stringify from 'safe-stable-stringify'; import { type Opaque } from 'type-fest'; import { JSON } from './json-schema-types.js'; /** * This function accepts any JS string and encodes it in base64, using a UTF8 * representation of the string to derive the underlying binary data. */ export function b64Encode(it: T) { return Buffer.from(it, 'utf-8').toString('base64') as B64Of; } /** * This function accepts any b64 encoded string, as generated by * {@link b64Encode}, and returns the original JS string that was encoded. */ export function b64Decode>(it: T) { return Buffer.from(it, 'base64').toString('utf-8') as T[typeof meta]; } export function b64UrlEncode(it: T) { return b64Encode(it).replace('+', '-').replace('/', '_') as B64UrlOf; } export function b64UrlDecode>(it: T) { const b64String = it.replace('-', '+').replace('_', '/') as B64Of< (typeof it)[typeof meta] >; return b64Decode(b64String); } export function b64EncodeArrayBuffer(it: T) { return Buffer.from(it).toString('base64') as B64Of; } /** * Converts a value to JSON, while preserving its type for future inspection. * * Sometimes, we need to stringify a value (e.g., to use the string as a key), * but we'd still like to Typescript to track the original type that we * stringified, so that we can have type checking on the data we'll get back * if/when we JSON.parse the string later. That's what this `jsonStringify` * helper function does. See {@link jsonParse}. * * NB: technically, this should return a JsonOf>, but we don't do * that for now because using Jsonify almost always runs up against TS stack * limits. * * @param it The value to stringify. */ export function jsonStringify(it: T) { // eslint-disable-next-line no-restricted-syntax return stringify(it) as JsonOf; } /** * Identical to {@link jsonStringify}, except that it does not normalize the * order of object keys in the final, returned string. Therefore, e.g., * `{ a: 0, b: 0 }` and `{ b: 0, a: 0 }` will produce different strings. This is * usually not what you want -- it prevents the resulting string from being used * reliably as a cache key, e.g. -- but may give slightly better performance. */ export function jsonStringifyUnstable(it: T) { // eslint-disable-next-line no-restricted-syntax return JSON.stringify(it) as JsonOf; } /** * Parses the JSON, and returns its original type, for JSON generated by * {@link jsonStringify}. */ export function jsonParse>(it: T) { // eslint-disable-next-line no-restricted-syntax return JSON.parse(it) as (typeof it)[typeof meta]; } /** * Returns the parsed value if JSON parsing succeeds; else undefined. */ export function tryJsonParse(it: string): JSON | undefined { try { // eslint-disable-next-line no-restricted-syntax return JSON.parse(it); } catch (e) { return undefined; } } /** * Constructs an array whose elements are tuples of the characters * and the number of times they appear in order of the string. * IE: teeest = [['t', 1], ['e', 3], ['s', 1], ['t', 1]] */ export function runEncode(text: string): [string, number][] { const ret: [string, number][] = []; let lastChar = null; let charCount = 0; for (const c of text) { if (c === lastChar) { charCount++; } else { if (lastChar !== null) { ret.push([lastChar, charCount]); } lastChar = c; charCount = 1; } } if (lastChar) { ret.push([lastChar, charCount]); } return ret; } /** * Escape any characters that have a special meaning in regex syntax. */ export function regexEscape(text: string) { return text.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); } declare const meta: unique symbol; export type JsonOf = Opaque & { readonly [meta]: T }; export type B64Of = Opaque & { readonly [meta]: T }; export type B64UrlOf = Opaque & { readonly [meta]: T };