@meantjs/format 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/LICENSE +202 -0
- package/README.md +123 -2
- package/package.json +49 -4
- package/src/account.mjs +78 -0
- package/src/amount.mjs +92 -0
- package/src/bytes.mjs +131 -0
- package/src/errors.mjs +49 -0
- package/src/formatter.mjs +272 -0
- package/src/index.d.mts +169 -0
- package/src/index.mjs +17 -0
- package/src/names.mjs +59 -0
- package/src/principal.mjs +117 -0
- package/src/subaccount.mjs +49 -0
- package/src/time.mjs +258 -0
- package/src/util.mjs +139 -0
package/src/bytes.mjs
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// `bytes` (spec §5.3): a blob shown and read as utf8, hex or base64.
|
|
2
|
+
//
|
|
3
|
+
// utf8 input is plain text encoded as UTF-8; input starting with 0x is read as hex.
|
|
4
|
+
// Display is the decoded text when the bytes are valid UTF-8 without control
|
|
5
|
+
// characters, else 0x followed by lowercase hex.
|
|
6
|
+
// hex input and display carry a 0x prefix (the prefix is optional on input).
|
|
7
|
+
// base64 RFC 4648 standard alphabet; padding optional on input, always written on display.
|
|
8
|
+
// max_len counts bytes after encoding.
|
|
9
|
+
//
|
|
10
|
+
// Two display rules go beyond the literal text so that parse(display(x)) = x always holds and so
|
|
11
|
+
// that nothing misleading is shown (both are listed in docs/proposals/w3a-spec-5-3-clarifications.md):
|
|
12
|
+
// - utf8 text that itself starts with 0x/0X is displayed as hex, because as text it would be read back as hex;
|
|
13
|
+
// - besides control characters (Cc), text with format characters (Cf, for example bidirectional
|
|
14
|
+
// overrides, zero-width joiners and the byte order mark) or line and paragraph separators
|
|
15
|
+
// (Zl, Zp) is displayed as hex.
|
|
16
|
+
|
|
17
|
+
import { CODES, fail } from "./errors.mjs";
|
|
18
|
+
import { UNSAFE_DISPLAY_CHAR, bytesToHex, hexToBytes, toBytes } from "./util.mjs";
|
|
19
|
+
|
|
20
|
+
export const BYTES_ENCODINGS = ["utf8", "hex", "base64"];
|
|
21
|
+
|
|
22
|
+
/** Bound on the size of a text input (bounded work, spec §11.6). */
|
|
23
|
+
const MAX_BYTES_INPUT_CHARS = 4 * 1024 * 1024;
|
|
24
|
+
const HEX_PREFIX = /^0[xX]/;
|
|
25
|
+
|
|
26
|
+
const B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
27
|
+
const B64_VALUE = (() => {
|
|
28
|
+
const t = new Int8Array(128).fill(-1);
|
|
29
|
+
for (let i = 0; i < 64; i++) t[B64.charCodeAt(i)] = i;
|
|
30
|
+
return t;
|
|
31
|
+
})();
|
|
32
|
+
|
|
33
|
+
function base64Encode(bytes) {
|
|
34
|
+
let out = "";
|
|
35
|
+
for (let i = 0; i < bytes.length; i += 3) {
|
|
36
|
+
const n = (bytes[i] << 16) | ((bytes[i + 1] ?? 0) << 8) | (bytes[i + 2] ?? 0);
|
|
37
|
+
out += B64[(n >> 18) & 63] + B64[(n >> 12) & 63];
|
|
38
|
+
out += i + 1 < bytes.length ? B64[(n >> 6) & 63] : "=";
|
|
39
|
+
out += i + 2 < bytes.length ? B64[n & 63] : "=";
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Strict: standard alphabet, canonical padding or none, no stray characters, zero spare bits. */
|
|
45
|
+
function base64Decode(text) {
|
|
46
|
+
const m = /^([A-Za-z0-9+/]*)(={0,2})$/.exec(text);
|
|
47
|
+
if (!m) {
|
|
48
|
+
fail(CODES.BASE64, "not base64: use the standard alphabet (A-Z, a-z, 0-9, + and /) with no spaces, line breaks or URL-safe characters");
|
|
49
|
+
}
|
|
50
|
+
const [, body, pad] = m;
|
|
51
|
+
const rem = body.length % 4;
|
|
52
|
+
if (rem === 1) fail(CODES.BASE64, "not base64: the length is impossible (one character too many or too few)");
|
|
53
|
+
if (pad && (rem === 0 || pad.length !== 4 - rem)) fail(CODES.BASE64, "not base64: the '=' padding is wrong");
|
|
54
|
+
const out = new Uint8Array(Math.floor((body.length * 3) / 4));
|
|
55
|
+
let acc = 0;
|
|
56
|
+
let bits = 0;
|
|
57
|
+
let o = 0;
|
|
58
|
+
for (let i = 0; i < body.length; i++) {
|
|
59
|
+
acc = (acc << 6) | B64_VALUE[body.charCodeAt(i)];
|
|
60
|
+
bits += 6;
|
|
61
|
+
if (bits >= 8) {
|
|
62
|
+
out[o++] = (acc >> (bits - 8)) & 255;
|
|
63
|
+
bits -= 8;
|
|
64
|
+
}
|
|
65
|
+
acc &= (1 << bits) - 1;
|
|
66
|
+
}
|
|
67
|
+
if (acc !== 0) fail(CODES.BASE64, "not canonical base64: the unused trailing bits must be zero");
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function parseHex(text, what) {
|
|
72
|
+
const bytes = hexToBytes(text.replace(HEX_PREFIX, ""));
|
|
73
|
+
if (bytes === null) fail(CODES.HEX, `${what} is not hexadecimal: use an even number of digits 0-9 and a-f, for example "0x1a2b" (no spaces)`);
|
|
74
|
+
return bytes;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function checkMaxLen(maxLen) {
|
|
78
|
+
if (maxLen === undefined || maxLen === null) return null;
|
|
79
|
+
let n = maxLen;
|
|
80
|
+
if (typeof n === "bigint" || (typeof n === "string" && /^(?:0|[1-9][0-9]*)$/.test(n))) n = Number(n);
|
|
81
|
+
if (!Number.isSafeInteger(n) || n < 0) fail(CODES.ARG, `max_len must be a non-negative integer, got ${String(maxLen)}`);
|
|
82
|
+
return n;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* @param {string} input
|
|
87
|
+
* @param {{ encoding: "utf8"|"hex"|"base64", maxLen?: number }} opts `maxLen` is the tag's max_len, in bytes
|
|
88
|
+
* @returns {Uint8Array}
|
|
89
|
+
*/
|
|
90
|
+
export function parseBytes(input, { encoding, maxLen } = {}) {
|
|
91
|
+
if (!BYTES_ENCODINGS.includes(encoding)) fail(CODES.ARG, `bytes encoding must be one of ${BYTES_ENCODINGS.join(", ")}, got ${JSON.stringify(encoding)}`);
|
|
92
|
+
const max = checkMaxLen(maxLen);
|
|
93
|
+
if (typeof input !== "string") fail(CODES.TYPE, `bytes input must be a string, got ${typeof input}`);
|
|
94
|
+
if (input.length > MAX_BYTES_INPUT_CHARS) fail(CODES.TOO_LONG, `bytes input is longer than ${MAX_BYTES_INPUT_CHARS} characters`);
|
|
95
|
+
let bytes;
|
|
96
|
+
if (encoding === "utf8") {
|
|
97
|
+
if (HEX_PREFIX.test(input)) bytes = parseHex(input, "text starting with 0x (read as hex)");
|
|
98
|
+
else {
|
|
99
|
+
// TextEncoder would replace a lone surrogate with U+FFFD without saying so.
|
|
100
|
+
if (!input.isWellFormed()) fail(CODES.UTF8, "the text contains an unpaired surrogate and cannot be encoded as UTF-8");
|
|
101
|
+
bytes = new TextEncoder().encode(input);
|
|
102
|
+
}
|
|
103
|
+
} else if (encoding === "hex") {
|
|
104
|
+
bytes = parseHex(input, "the value");
|
|
105
|
+
} else {
|
|
106
|
+
bytes = base64Decode(input);
|
|
107
|
+
}
|
|
108
|
+
if (max !== null && bytes.length > max) fail(CODES.MAX_LEN, `the value is ${bytes.length} bytes after encoding but at most ${max} are allowed`);
|
|
109
|
+
return bytes;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* @param {Uint8Array | number[]} value
|
|
114
|
+
* @param {{ encoding: "utf8"|"hex"|"base64" }} opts
|
|
115
|
+
* @returns {string}
|
|
116
|
+
*/
|
|
117
|
+
export function displayBytes(value, { encoding } = {}) {
|
|
118
|
+
if (!BYTES_ENCODINGS.includes(encoding)) fail(CODES.ARG, `bytes encoding must be one of ${BYTES_ENCODINGS.join(", ")}, got ${JSON.stringify(encoding)}`);
|
|
119
|
+
const bytes = toBytes(value, "bytes");
|
|
120
|
+
if (encoding === "hex") return `0x${bytesToHex(bytes)}`;
|
|
121
|
+
if (encoding === "base64") return base64Encode(bytes);
|
|
122
|
+
let text;
|
|
123
|
+
try {
|
|
124
|
+
// ignoreBOM keeps a leading U+FEFF in the string instead of silently dropping its three bytes
|
|
125
|
+
text = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
|
|
126
|
+
} catch {
|
|
127
|
+
return `0x${bytesToHex(bytes)}`;
|
|
128
|
+
}
|
|
129
|
+
if (UNSAFE_DISPLAY_CHAR.test(text) || HEX_PREFIX.test(text)) return `0x${bytesToHex(bytes)}`;
|
|
130
|
+
return text;
|
|
131
|
+
}
|
package/src/errors.mjs
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Every rejection in this package is a FormatError with a stable `code`.
|
|
2
|
+
// Callers (arm C tool layer, the ICRC-21 generator) switch on `code`; `message` is written for a
|
|
3
|
+
// model or a person and says how to fix the input.
|
|
4
|
+
|
|
5
|
+
export class FormatError extends Error {
|
|
6
|
+
/**
|
|
7
|
+
* @param {string} code one of CODES
|
|
8
|
+
* @param {string} message
|
|
9
|
+
*/
|
|
10
|
+
constructor(code, message) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = "FormatError";
|
|
13
|
+
this.code = code;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const CODES = Object.freeze({
|
|
18
|
+
// shared
|
|
19
|
+
TYPE: "E_TYPE", // wrong JavaScript type for an input, for example a number where a decimal string is required
|
|
20
|
+
SYNTAX: "E_SYNTAX", // the text does not match the grammar
|
|
21
|
+
RANGE: "E_RANGE", // outside the range of the Candid type or of the representable calendar
|
|
22
|
+
TOO_LONG: "E_TOO_LONG", // input longer than the bound on work
|
|
23
|
+
ARG: "E_ARG", // a tag argument is missing or invalid
|
|
24
|
+
TAG_TYPE: "E_TAG_TYPE", // the tag does not apply to this Candid type
|
|
25
|
+
TAG_CONFLICT: "E_TAG_CONFLICT", // two formatting tags at one position
|
|
26
|
+
REF_UNRESOLVED: "E_REF_UNRESOLVED", // a dynamic value was not supplied by the caller
|
|
27
|
+
// amount
|
|
28
|
+
EXCESS_PRECISION: "E_EXCESS_PRECISION", // more fractional digits than decimals (never rounded)
|
|
29
|
+
NEGATIVE: "E_NEGATIVE", // negative input for an unsigned Candid type
|
|
30
|
+
SYMBOL_MISMATCH: "E_SYMBOL_MISMATCH", // the symbol in the input is not the token's symbol
|
|
31
|
+
// time
|
|
32
|
+
INEXACT: "E_INEXACT", // the value cannot be represented exactly in the requested unit
|
|
33
|
+
CALENDAR_UNIT: "E_CALENDAR_UNIT", // years and months have no fixed length
|
|
34
|
+
NEEDS_NOW: "E_NEEDS_NOW", // relative time without a `now`
|
|
35
|
+
// account, subaccount, bytes
|
|
36
|
+
CHECKSUM: "E_CHECKSUM", // checksum does not match
|
|
37
|
+
CHECKSUM_MISSING: "E_CHECKSUM_MISSING",
|
|
38
|
+
NOT_CANONICAL: "E_NOT_CANONICAL", // a valid value written in a non-canonical way
|
|
39
|
+
PRINCIPAL: "E_PRINCIPAL", // invalid principal text
|
|
40
|
+
HEX: "E_HEX",
|
|
41
|
+
BASE64: "E_BASE64",
|
|
42
|
+
UTF8: "E_UTF8", // text that cannot be encoded as UTF-8 (lone surrogate)
|
|
43
|
+
LENGTH: "E_LENGTH", // wrong length in bytes
|
|
44
|
+
MAX_LEN: "E_MAX_LEN", // longer than the tag's max_len
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
export function fail(code, message) {
|
|
48
|
+
throw new FormatError(code, message);
|
|
49
|
+
}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
// formatterFor(node): the one entry point for consumers (arm C, the ICRC-21 consent generator).
|
|
2
|
+
//
|
|
3
|
+
// `node` is a position from the resolved form (docs/profile-v0.md §10): its `candid` type and its
|
|
4
|
+
// `tags`. The function picks the value tag at that position and returns a { parse, display } pair
|
|
5
|
+
// bound to the tag's arguments. Dynamic arguments such as { "from": "query:icrc1_decimals" } are
|
|
6
|
+
// resolved by the caller (the library does no I/O) and passed in `refs`, keyed by the `from` string.
|
|
7
|
+
|
|
8
|
+
import { CODES, FormatError, fail } from "./errors.mjs";
|
|
9
|
+
import { AMOUNT_TYPES, checkDecimals, checkSymbol, displayAmount, parseAmount } from "./amount.mjs";
|
|
10
|
+
import { TIME_TYPES, displayDuration, displayTimestamp, parseDuration, parseTimestamp, unitFactor } from "./time.mjs";
|
|
11
|
+
import { displayAccount, parseAccount } from "./account.mjs";
|
|
12
|
+
import { displaySubaccount, parseSubaccount } from "./subaccount.mjs";
|
|
13
|
+
import { BYTES_ENCODINGS, checkMaxLen, displayBytes, parseBytes } from "./bytes.mjs";
|
|
14
|
+
|
|
15
|
+
/** The tags that give a position a text form, in spec §5.1. */
|
|
16
|
+
export const VALUE_TAGS = Object.freeze(["amount", "timestamp", "duration", "account", "subaccount", "bytes"]);
|
|
17
|
+
|
|
18
|
+
/** A dynamic value ({ from }) replaced by the caller-supplied one; literals pass through. */
|
|
19
|
+
function resolveArg(arg, refs, where) {
|
|
20
|
+
if (arg === null || typeof arg !== "object" || Array.isArray(arg)) return arg;
|
|
21
|
+
const from = arg.from;
|
|
22
|
+
if (typeof from !== "string") fail(CODES.ARG, `${where} is an object without a "from" string`);
|
|
23
|
+
if (from === "now" || from === "caller") fail(CODES.ARG, `${where}: { "from": "${from}" } is not valid here (only default.value may use it)`);
|
|
24
|
+
if (!Object.hasOwn(refs, from)) {
|
|
25
|
+
fail(CODES.REF_UNRESOLVED, `${where} needs the value of "${from}", which the caller did not supply (call it once per canister and pass it in \`refs\`)`);
|
|
26
|
+
}
|
|
27
|
+
return refs[from];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function requireCandid(node, allowed, tag) {
|
|
31
|
+
if (!allowed.includes(node.candid)) fail(CODES.TAG_TYPE, `the ${tag} tag applies to ${allowed.join(", ")}, not ${node.candid}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function requireUnit(unit, tag) {
|
|
35
|
+
if (unit === undefined) fail(CODES.ARG, `the ${tag} tag needs a unit`);
|
|
36
|
+
unitFactor(unit);
|
|
37
|
+
return unit;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @typedef {object} Formatter
|
|
42
|
+
* @property {string} tag the value tag: amount, timestamp, duration, account, subaccount or bytes
|
|
43
|
+
* @property {string} candid the Candid type of the position (`opt` is the node's `optional` flag, not handled here)
|
|
44
|
+
* @property {Readonly<Record<string, unknown>>} args the tag's arguments with dynamic values resolved
|
|
45
|
+
* @property {(input: string, ctx?: { now?: bigint }) => any} parse throws FormatError. amount, timestamp,
|
|
46
|
+
* duration: decimal string in base units / tagged unit; account: { owner, subaccount }; subaccount, bytes: Uint8Array
|
|
47
|
+
* @property {(value: any) => string} display throws FormatError
|
|
48
|
+
* @property {(input: string, ctx?: { now?: bigint }) => { ok: true, value: any } | { ok: false, error: { code: string, message: string } }} safeParse
|
|
49
|
+
* @property {(value: any) => { ok: true, value: string } | { ok: false, error: { code: string, message: string } }} safeDisplay
|
|
50
|
+
* @property {() => Description} describe the text this formatter reads, in words, with examples (see `describeInput`)
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @typedef {object} Description
|
|
55
|
+
* @property {string} summary one paragraph for a form hint or a tool input's description
|
|
56
|
+
* @property {string[]} examples inputs `parse` accepts (a relative time needs `now`)
|
|
57
|
+
* @property {string} [pattern] an ECMAScript regular expression every accepted input matches (JSON Schema `pattern`).
|
|
58
|
+
* A necessary condition only: a matching input can still be refused (a range, a checksum). Given for amount and
|
|
59
|
+
* subaccount, whose grammar is regular.
|
|
60
|
+
*/
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @param {{ candid: string, tags?: Record<string, { args?: Record<string, unknown> }> }} node
|
|
64
|
+
* @param {{ refs?: Record<string, unknown>, now?: bigint }} [options]
|
|
65
|
+
* `refs`: resolved dynamic values keyed by their `from` string, for example
|
|
66
|
+
* `{ "query:icrc1_decimals": 8, "query:icrc1_symbol": "ICP" }`.
|
|
67
|
+
* `now`: default clock (nanoseconds, bigint) for relative timestamps; `parse(input, { now })` overrides it.
|
|
68
|
+
* @returns {Formatter | null} null when the position has no value tag (render it as Candid text, spec §6)
|
|
69
|
+
*/
|
|
70
|
+
export function formatterFor(node, { refs = {}, now: defaultNow } = {}) {
|
|
71
|
+
if (node === null || typeof node !== "object" || typeof node.candid !== "string") fail(CODES.TYPE, "node must be a resolved-form position with a `candid` type");
|
|
72
|
+
const tags = node.tags ?? {};
|
|
73
|
+
const present = VALUE_TAGS.filter((t) => Object.hasOwn(tags, t));
|
|
74
|
+
if (present.length === 0) return null;
|
|
75
|
+
if (present.length > 1) fail(CODES.TAG_CONFLICT, `a position cannot carry more than one value tag, found ${present.join(", ")}`);
|
|
76
|
+
const tag = present[0];
|
|
77
|
+
const raw = tags[tag]?.args ?? {};
|
|
78
|
+
const arg = (name) => resolveArg(raw[name], refs, `${tag}.${name}`);
|
|
79
|
+
|
|
80
|
+
let args;
|
|
81
|
+
let parse;
|
|
82
|
+
let display;
|
|
83
|
+
switch (tag) {
|
|
84
|
+
case "amount": {
|
|
85
|
+
requireCandid(node, AMOUNT_TYPES, tag);
|
|
86
|
+
if (raw.decimals === undefined) fail(CODES.ARG, "the amount tag needs decimals");
|
|
87
|
+
const decimals = checkDecimals(arg("decimals"));
|
|
88
|
+
const symbol = checkSymbol(arg("symbol"));
|
|
89
|
+
const o = { decimals, symbol, candid: node.candid };
|
|
90
|
+
args = { decimals, symbol };
|
|
91
|
+
parse = (input) => parseAmount(input, o);
|
|
92
|
+
display = (value) => displayAmount(value, o);
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
case "timestamp": {
|
|
96
|
+
requireCandid(node, TIME_TYPES, tag);
|
|
97
|
+
if (raw.epoch !== undefined && raw.epoch !== "unix") fail(CODES.ARG, `timestamp epoch must be "unix", got ${JSON.stringify(raw.epoch)}`);
|
|
98
|
+
const unit = requireUnit(arg("unit"), tag);
|
|
99
|
+
const o = { unit, candid: node.candid };
|
|
100
|
+
args = { unit, epoch: "unix" };
|
|
101
|
+
parse = (input, ctx = {}) => parseTimestamp(input, { ...o, now: ctx.now ?? defaultNow });
|
|
102
|
+
display = (value) => displayTimestamp(value, o);
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
case "duration": {
|
|
106
|
+
requireCandid(node, TIME_TYPES, tag);
|
|
107
|
+
const unit = requireUnit(arg("unit"), tag);
|
|
108
|
+
const o = { unit, candid: node.candid };
|
|
109
|
+
args = { unit };
|
|
110
|
+
parse = (input) => parseDuration(input, o);
|
|
111
|
+
display = (value) => displayDuration(value, o);
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
case "account": {
|
|
115
|
+
requireCandid(node, ["record"], tag);
|
|
116
|
+
if (raw.encoding !== undefined && raw.encoding !== "icrc1") fail(CODES.ARG, `account encoding must be "icrc1", got ${JSON.stringify(raw.encoding)}`);
|
|
117
|
+
args = { encoding: "icrc1" };
|
|
118
|
+
parse = (input) => parseAccount(input);
|
|
119
|
+
display = (value) => displayAccount(value);
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
case "subaccount": {
|
|
123
|
+
requireCandid(node, ["blob"], tag);
|
|
124
|
+
args = {};
|
|
125
|
+
parse = (input) => parseSubaccount(input);
|
|
126
|
+
display = (value) => displaySubaccount(value);
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
case "bytes": {
|
|
130
|
+
requireCandid(node, ["blob"], tag);
|
|
131
|
+
const encoding = arg("encoding");
|
|
132
|
+
if (!BYTES_ENCODINGS.includes(encoding)) fail(CODES.ARG, `the bytes tag needs encoding utf8, hex or base64, got ${JSON.stringify(encoding)}`);
|
|
133
|
+
const maxLen = raw.max_len;
|
|
134
|
+
args = { encoding, ...(maxLen === undefined ? {} : { max_len: maxLen }) };
|
|
135
|
+
parse = (input) => parseBytes(input, { encoding, maxLen });
|
|
136
|
+
display = (value) => displayBytes(value, { encoding });
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const safe = (fn) => (...a) => {
|
|
142
|
+
try {
|
|
143
|
+
return { ok: true, value: fn(...a) };
|
|
144
|
+
} catch (e) {
|
|
145
|
+
if (e instanceof FormatError) return { ok: false, error: { code: e.code, message: e.message } };
|
|
146
|
+
throw e;
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
// Keep only the examples this position accepts: a range (nat8, nat32 in ns) or a max_len can refuse one. A relative time
|
|
150
|
+
// is checked against the formatter's own clock when it was given one (`parse` uses it), else against a fixed instant
|
|
151
|
+
// (2026-01-01T00:00:00Z), since describing must not read a clock.
|
|
152
|
+
const description = describeInput(tag, args, node.candid, { accepts: (x) => safe(parse)(x, { now: defaultNow ?? DESCRIBE_NOW }).ok });
|
|
153
|
+
return Object.freeze({
|
|
154
|
+
tag,
|
|
155
|
+
candid: node.candid,
|
|
156
|
+
args: Object.freeze(args),
|
|
157
|
+
describe: () => ({ ...description, examples: [...description.examples] }),
|
|
158
|
+
parse,
|
|
159
|
+
display,
|
|
160
|
+
safeParse: safe(parse),
|
|
161
|
+
safeDisplay: safe(display),
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ---------------------------------------------------------------- describe
|
|
166
|
+
|
|
167
|
+
/** The Candid types each value tag applies to (the `requireCandid` calls in `formatterFor`). */
|
|
168
|
+
const CANDID_OF = { amount: AMOUNT_TYPES, timestamp: TIME_TYPES, duration: TIME_TYPES, account: ["record"], subaccount: ["blob"], bytes: ["blob"] };
|
|
169
|
+
/** 2026-01-01T00:00:00Z in ns: the reference instant relative examples are checked against. */
|
|
170
|
+
const DESCRIBE_NOW = 1_767_225_600_000_000_000n;
|
|
171
|
+
const SIGNED = new Set(["int", "int8", "int16", "int32", "int64"]);
|
|
172
|
+
// ECMAScript syntax characters only: escaping anything else (such as "-") is an error under the `u` flag, which JSON
|
|
173
|
+
// Schema validators such as Ajv use.
|
|
174
|
+
const reEscape = (s) => s.replace(/[\\^$.*+?()[\]{}|/]/g, "\\$&");
|
|
175
|
+
/** The symbol as `parseAmount` compares it: ASCII letters in either case, everything else exactly. */
|
|
176
|
+
const symbolPattern = (symbol) => [...symbol].map((c) => (/[A-Za-z]/.test(c) ? `[${c.toUpperCase()}${c.toLowerCase()}]` : reEscape(c))).join("");
|
|
177
|
+
const quoted = (xs) => xs.map((x) => JSON.stringify(x)).join(", ");
|
|
178
|
+
|
|
179
|
+
/** The grammar in words (no examples), candidate examples, and a pattern where the grammar is regular. */
|
|
180
|
+
function grammar(tag, args, candid) {
|
|
181
|
+
const signed = SIGNED.has(candid);
|
|
182
|
+
const finer = (what) => (args.unit === "ns" ? "" : ` Stored in ${args.unit}: a ${what} finer than that is an error, not rounded.`);
|
|
183
|
+
switch (tag) {
|
|
184
|
+
case "amount": {
|
|
185
|
+
const { decimals: d, symbol } = args;
|
|
186
|
+
const examples = d === 0 ? ["1", "250"] : ["1", d === 1 ? "1.5" : "1.25"];
|
|
187
|
+
if (symbol !== undefined) examples.push(`${examples[1]} ${symbol}`);
|
|
188
|
+
return {
|
|
189
|
+
words:
|
|
190
|
+
`Amount in ${symbol === undefined ? "the token's units" : symbol}: ` +
|
|
191
|
+
(d === 0 ? "a whole number (this token has no decimal places)" : `a decimal number with at most ${d} digit${d === 1 ? "" : "s"} after the point`) +
|
|
192
|
+
(symbol === undefined ? "" : `, optionally followed by the symbol ${symbol}`) +
|
|
193
|
+
`. Digits on both sides of a point, no thousands separators or exponents${signed ? ', "-" for a negative amount' : ""}. ` +
|
|
194
|
+
"Never rounded: more digits after the point is an error, even zeros.",
|
|
195
|
+
examples,
|
|
196
|
+
pattern:
|
|
197
|
+
`^\\s*${signed ? "-?" : ""}[0-9]+${d === 0 ? "" : `(?:\\.[0-9]{1,${d}})?`}` + (symbol === undefined ? "" : `(?:\\s+${symbolPattern(symbol)})?`) + "\\s*$",
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
case "timestamp":
|
|
201
|
+
return {
|
|
202
|
+
words:
|
|
203
|
+
'Time in RFC 3339 with an explicit zone, or relative to the current time as "+" and an ISO 8601 duration with ' +
|
|
204
|
+
"weeks, days, hours, minutes and seconds only (no years or months; the seconds may have a fraction)." +
|
|
205
|
+
finer("time"),
|
|
206
|
+
examples: ["2026-12-25T10:00:00Z", "+PT24H", "+P7D"],
|
|
207
|
+
};
|
|
208
|
+
case "duration":
|
|
209
|
+
return {
|
|
210
|
+
words:
|
|
211
|
+
`ISO 8601 duration with weeks, days, hours, minutes and seconds only (no years or months); the seconds may have a fraction${signed ? '; "-" for a negative duration' : ""}.` +
|
|
212
|
+
finer("duration"),
|
|
213
|
+
examples: ["PT24H", "P7D", "PT1M30S"],
|
|
214
|
+
};
|
|
215
|
+
case "account":
|
|
216
|
+
return {
|
|
217
|
+
words: 'ICRC-1 account in text form: a principal for its default subaccount, or "principal-checksum.subaccounthex" for another subaccount.',
|
|
218
|
+
examples: ["aaaaa-aa", "2vxsx-fae"],
|
|
219
|
+
};
|
|
220
|
+
case "subaccount":
|
|
221
|
+
return {
|
|
222
|
+
words: 'Subaccount as "#n" or n (a number below 2^64 without leading zeros, stored as 32 big-endian bytes), or as exactly 64 hex characters, optionally after "0x".',
|
|
223
|
+
examples: ["#0", "#7", "0".repeat(63) + "1"],
|
|
224
|
+
pattern: "^\\s*(?:#?(?:0|[1-9][0-9]*)|(?:0[xX])?[0-9a-fA-F]{64})\\s*$",
|
|
225
|
+
};
|
|
226
|
+
case "bytes": {
|
|
227
|
+
const max = args.max_len === undefined || args.max_len === null ? "" : ` At most ${args.max_len} bytes after encoding.`;
|
|
228
|
+
if (args.encoding === "hex") return { words: `Bytes as hexadecimal, an even number of digits, optionally after "0x".${max}`, examples: ["0x1a2b", "1a2b"] };
|
|
229
|
+
if (args.encoding === "base64") return {
|
|
230
|
+
words: `Bytes as standard base64 (RFC 4648: A-Z, a-z, 0-9, "+" and "/"; not the URL-safe "-" and "_"), with "=" padding or none, no spaces or line breaks, and the unused bits of the last character zero.${max}`,
|
|
231
|
+
examples: ["aGk="],
|
|
232
|
+
};
|
|
233
|
+
return { words: `Text, stored as its UTF-8 bytes. Text that starts with "0x" is read as hex bytes instead.${max}`, examples: ["invoice 42", "0x1a2b"] };
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
throw new Error(`no description for the ${tag} tag`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* The input grammar of a value tag in words (spec §5.3), with the tag's resolved arguments filled in, so a form hint,
|
|
241
|
+
* a tool schema and the parser share one description. The summary quotes exactly the examples kept.
|
|
242
|
+
*
|
|
243
|
+
* @param {string} tag
|
|
244
|
+
* @param {Readonly<Record<string, any>>} args the tag's arguments, dynamic values resolved (`Formatter.args`)
|
|
245
|
+
* @param {string} candid
|
|
246
|
+
* @param {{ accepts?: (input: string) => boolean }} [options] keeps only the examples it accepts (`Formatter.describe`
|
|
247
|
+
* passes its own parser, so a range or `max_len` that refuses an example leaves it out); default: all of them
|
|
248
|
+
* @returns {Description}
|
|
249
|
+
*/
|
|
250
|
+
export function describeInput(tag, args, candid, { accepts = () => true } = {}) {
|
|
251
|
+
// The same checks and normalisation formatterFor applies (decimals may arrive as "0" or 0n), so arguments it would
|
|
252
|
+
// refuse are refused here too, never described as another grammar.
|
|
253
|
+
if (VALUE_TAGS.includes(tag)) {
|
|
254
|
+
requireCandid({ candid }, CANDID_OF[tag], tag);
|
|
255
|
+
const a = args ?? {};
|
|
256
|
+
if (tag === "amount") args = { ...a, decimals: checkDecimals(a.decimals), symbol: checkSymbol(a.symbol) };
|
|
257
|
+
if (tag === "timestamp" || tag === "duration") requireUnit(a.unit, tag);
|
|
258
|
+
if (tag === "timestamp" && a.epoch !== undefined && a.epoch !== "unix") fail(CODES.ARG, `timestamp epoch must be "unix", got ${JSON.stringify(a.epoch)}`);
|
|
259
|
+
if (tag === "account" && a.encoding !== undefined && a.encoding !== "icrc1") fail(CODES.ARG, `account encoding must be "icrc1", got ${JSON.stringify(a.encoding)}`);
|
|
260
|
+
if (tag === "bytes") {
|
|
261
|
+
if (!BYTES_ENCODINGS.includes(a.encoding)) fail(CODES.ARG, `the bytes tag needs encoding utf8, hex or base64, got ${JSON.stringify(a.encoding)}`);
|
|
262
|
+
checkMaxLen(a.max_len);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
const g = grammar(tag, args, candid);
|
|
266
|
+
const examples = g.examples.filter((x) => accepts(x));
|
|
267
|
+
return {
|
|
268
|
+
summary: g.words + (examples.length > 0 ? ` For example ${quoted(examples)}.` : ""),
|
|
269
|
+
examples,
|
|
270
|
+
...(g.pattern === undefined ? {} : { pattern: g.pattern }),
|
|
271
|
+
};
|
|
272
|
+
}
|
package/src/index.d.mts
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// Types for @meantjs/format. Hand-written; the source is plain ES modules (src/*.mjs).
|
|
2
|
+
// test/types.test.mjs keeps the declared names equal to the runtime exports, and `npm run types` compiles a consumer
|
|
3
|
+
// against these declarations under strict TypeScript.
|
|
4
|
+
|
|
5
|
+
/** The value tags (spec §5.1). */
|
|
6
|
+
export type ValueTag = "amount" | "timestamp" | "duration" | "account" | "subaccount" | "bytes";
|
|
7
|
+
export type TimeUnit = "ns" | "us" | "ms" | "s";
|
|
8
|
+
export type BytesEncoding = "utf8" | "hex" | "base64";
|
|
9
|
+
/** Bytes as the library takes them: a Uint8Array (from any realm) or an array of byte values. */
|
|
10
|
+
export type ByteInput = Uint8Array | readonly number[];
|
|
11
|
+
/** An ICRC-1 account as `parseAccount` returns it: `subaccount` null is the default subaccount. */
|
|
12
|
+
export interface Account {
|
|
13
|
+
owner: string;
|
|
14
|
+
subaccount: Uint8Array | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** The stable rejection codes; `FormatError.code` is one of them. */
|
|
18
|
+
export const CODES: Readonly<{
|
|
19
|
+
TYPE: "E_TYPE";
|
|
20
|
+
SYNTAX: "E_SYNTAX";
|
|
21
|
+
RANGE: "E_RANGE";
|
|
22
|
+
TOO_LONG: "E_TOO_LONG";
|
|
23
|
+
ARG: "E_ARG";
|
|
24
|
+
TAG_TYPE: "E_TAG_TYPE";
|
|
25
|
+
TAG_CONFLICT: "E_TAG_CONFLICT";
|
|
26
|
+
REF_UNRESOLVED: "E_REF_UNRESOLVED";
|
|
27
|
+
EXCESS_PRECISION: "E_EXCESS_PRECISION";
|
|
28
|
+
NEGATIVE: "E_NEGATIVE";
|
|
29
|
+
SYMBOL_MISMATCH: "E_SYMBOL_MISMATCH";
|
|
30
|
+
INEXACT: "E_INEXACT";
|
|
31
|
+
CALENDAR_UNIT: "E_CALENDAR_UNIT";
|
|
32
|
+
NEEDS_NOW: "E_NEEDS_NOW";
|
|
33
|
+
CHECKSUM: "E_CHECKSUM";
|
|
34
|
+
CHECKSUM_MISSING: "E_CHECKSUM_MISSING";
|
|
35
|
+
NOT_CANONICAL: "E_NOT_CANONICAL";
|
|
36
|
+
PRINCIPAL: "E_PRINCIPAL";
|
|
37
|
+
HEX: "E_HEX";
|
|
38
|
+
BASE64: "E_BASE64";
|
|
39
|
+
UTF8: "E_UTF8";
|
|
40
|
+
LENGTH: "E_LENGTH";
|
|
41
|
+
MAX_LEN: "E_MAX_LEN";
|
|
42
|
+
}>;
|
|
43
|
+
export type FormatErrorCode = (typeof CODES)[keyof typeof CODES];
|
|
44
|
+
|
|
45
|
+
/** Every rejection in this package. `message` says how to fix the input. */
|
|
46
|
+
export declare class FormatError extends Error {
|
|
47
|
+
constructor(code: FormatErrorCode, message: string);
|
|
48
|
+
name: "FormatError";
|
|
49
|
+
code: FormatErrorCode;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface AmountOptions {
|
|
53
|
+
decimals: number | bigint | string;
|
|
54
|
+
symbol?: string;
|
|
55
|
+
/** The Candid type, for the sign and range (default `nat`). */
|
|
56
|
+
candid?: string;
|
|
57
|
+
}
|
|
58
|
+
/** "1.5" or "1.5 ICP" to base units as a decimal string. Excess precision throws; nothing is rounded. */
|
|
59
|
+
export function parseAmount(input: string, options: AmountOptions): string;
|
|
60
|
+
/** Base units to "1.5 ICP". A number is refused: pass a decimal string or a bigint. */
|
|
61
|
+
export function displayAmount(value: string | bigint, options: AmountOptions): string;
|
|
62
|
+
|
|
63
|
+
/** An integer between units; to a coarser unit it must be an exact multiple (E_INEXACT otherwise). */
|
|
64
|
+
export function convertUnit(value: string | bigint, from: TimeUnit, to: TimeUnit): string;
|
|
65
|
+
/** A clock reading in ns floored to `unit`, as `parseTimestamp` applies it to `now`. */
|
|
66
|
+
export function alignNow(nowNs: bigint, unit: TimeUnit): bigint;
|
|
67
|
+
/** An RFC 3339 time, or "+" and an ISO 8601 duration from `now` (ns, required then). Returns the count of `unit`. */
|
|
68
|
+
export function parseTimestamp(input: string, options: { unit: TimeUnit; candid?: string; now?: bigint }): string;
|
|
69
|
+
/** Canonical RFC 3339 UTC, for example "2026-10-09T12:00:00.5Z". */
|
|
70
|
+
export function displayTimestamp(value: string | bigint, options: { unit: TimeUnit; candid?: string }): string;
|
|
71
|
+
/** An ISO 8601 duration of W, D, H, M and S ("PT24H"); returns the count of `unit`. */
|
|
72
|
+
export function parseDuration(input: string, options: { unit: TimeUnit; candid?: string }): string;
|
|
73
|
+
/** Canonical ISO 8601: "P1D", "PT1H30M", "PT0S". */
|
|
74
|
+
export function displayDuration(value: string | bigint, options: { unit: TimeUnit; candid?: string }): string;
|
|
75
|
+
|
|
76
|
+
/** ICRC-1 account text; the checksum is verified and non-canonical text is refused. */
|
|
77
|
+
export function parseAccount(input: string): Account;
|
|
78
|
+
/** An account in the ICRC-1 textual encoding; a subaccount absent, null or all zero is the default. */
|
|
79
|
+
export function displayAccount(account: { owner: string | Uint8Array; subaccount?: ByteInput | null }): string;
|
|
80
|
+
/** Canonical principal text to its bytes; the CRC-32 must match. */
|
|
81
|
+
export function principalFromText(text: string): Uint8Array;
|
|
82
|
+
/** Up to 29 bytes to canonical principal text. */
|
|
83
|
+
export function principalToText(bytes: Uint8Array): string;
|
|
84
|
+
/** "#n", a decimal n, or 64 hex characters to 32 bytes. */
|
|
85
|
+
export function parseSubaccount(input: string): Uint8Array;
|
|
86
|
+
/** "#n" when the first 24 bytes are zero, else 64 lowercase hex characters. */
|
|
87
|
+
export function displaySubaccount(value: ByteInput): string;
|
|
88
|
+
/** Text in the tag's encoding to bytes; `maxLen` is the tag's `max_len`. */
|
|
89
|
+
export function parseBytes(input: string, options: { encoding: BytesEncoding; maxLen?: number }): Uint8Array;
|
|
90
|
+
export function displayBytes(value: ByteInput, options: { encoding: BytesEncoding }): string;
|
|
91
|
+
/**
|
|
92
|
+
* The value as a Uint8Array of this realm, viewing the same memory, or null. Accepts a Uint8Array made in another
|
|
93
|
+
* realm (an iframe, a `node:vm` context), which fails `instanceof Uint8Array`; refuses other typed arrays, DataViews,
|
|
94
|
+
* look-alike objects and views of a detached buffer.
|
|
95
|
+
*/
|
|
96
|
+
export function asUint8Array(value: unknown): Uint8Array | null;
|
|
97
|
+
|
|
98
|
+
/** The text a position reads, in words, with examples that parse there. */
|
|
99
|
+
export interface Description {
|
|
100
|
+
summary: string;
|
|
101
|
+
examples: string[];
|
|
102
|
+
/** An ECMAScript regular expression every accepted input matches (amount and subaccount only). */
|
|
103
|
+
pattern?: string;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export type SafeResult<T> = { ok: true; value: T } | { ok: false; error: { code: FormatErrorCode; message: string } };
|
|
107
|
+
|
|
108
|
+
/** What `parse` returns per tag: base units or a unit count as a decimal string, an account, or bytes. */
|
|
109
|
+
export type ParsedValue = string | Account | Uint8Array;
|
|
110
|
+
|
|
111
|
+
/** One position's formatter: `formatterFor` returns it with the tag's arguments resolved. */
|
|
112
|
+
export interface Formatter {
|
|
113
|
+
readonly tag: ValueTag;
|
|
114
|
+
readonly candid: string;
|
|
115
|
+
readonly args: Readonly<Record<string, unknown>>;
|
|
116
|
+
/** Throws FormatError. */
|
|
117
|
+
parse(input: string, context?: { now?: bigint }): ParsedValue;
|
|
118
|
+
/** Throws FormatError. */
|
|
119
|
+
display(value: unknown): string;
|
|
120
|
+
safeParse(input: string, context?: { now?: bigint }): SafeResult<ParsedValue>;
|
|
121
|
+
safeDisplay(value: unknown): SafeResult<string>;
|
|
122
|
+
describe(): Description;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** A position of a resolved form, as far as formatting reads it. */
|
|
126
|
+
export interface FormatNode {
|
|
127
|
+
candid: string;
|
|
128
|
+
tags?: Record<string, { args?: Record<string, unknown> } | undefined>;
|
|
129
|
+
[key: string]: unknown;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** The value tags, in spec order. */
|
|
133
|
+
export const VALUE_TAGS: readonly ValueTag[];
|
|
134
|
+
/**
|
|
135
|
+
* The formatter of a position, or null when it has no value tag (show it as Candid text). Throws FormatError for a
|
|
136
|
+
* missing ref, an invalid tag argument, a tag that does not fit the Candid type, or two value tags.
|
|
137
|
+
*
|
|
138
|
+
* @param options.refs dynamic tag values keyed by their `from`: `{ "query:icrc1_decimals": 8 }`
|
|
139
|
+
* @param options.now default clock for relative timestamps, ns since the Unix epoch
|
|
140
|
+
*/
|
|
141
|
+
export function formatterFor(node: FormatNode, options?: { refs?: Record<string, unknown>; now?: bigint }): Formatter | null;
|
|
142
|
+
/** `Formatter.describe` without a formatter; `accepts` keeps only the examples it returns true for. */
|
|
143
|
+
export function describeInput(tag: ValueTag, args: Readonly<Record<string, unknown>>, candid: string, options?: { accepts?: (input: string) => boolean }): Description;
|
|
144
|
+
|
|
145
|
+
/** One entry of a names set (spec section 5.3, D-045), as the set's method lists it. */
|
|
146
|
+
export interface NameEntry<V = unknown> {
|
|
147
|
+
name: string;
|
|
148
|
+
value: V;
|
|
149
|
+
show?: unknown;
|
|
150
|
+
kind?: unknown;
|
|
151
|
+
}
|
|
152
|
+
/** The result of resolveName: the input itself when it is a value, the one matching entry's value, or why not. */
|
|
153
|
+
export type NameResolution<V = unknown> =
|
|
154
|
+
| { value: string; direct: true }
|
|
155
|
+
| { value: V | unknown; entry: NameEntry<V> }
|
|
156
|
+
| { error: "not_found" }
|
|
157
|
+
| { error: "ambiguous"; entries: NameEntry<V>[] };
|
|
158
|
+
/** Unicode NFKC, the default full lowercase mapping, White_Space runs made one space, then trimmed. */
|
|
159
|
+
export function normalizeName(text: string): string;
|
|
160
|
+
/** Resolves a person's or an agent's text against a set's entries by the rule of spec section 5.3. */
|
|
161
|
+
export function resolveName<V>(
|
|
162
|
+
entries: NameEntry<V>[],
|
|
163
|
+
input: string,
|
|
164
|
+
opts?: { isValue?: (text: string) => boolean; kind?: (kind: unknown) => boolean; by?: "value" | "show" },
|
|
165
|
+
): NameResolution<V>;
|
|
166
|
+
/** The one entry with a name that a value belongs to, compared as Candid values (an account by owner and subaccount). */
|
|
167
|
+
export function ownerOf<V>(entries: NameEntry<V>[], value: unknown, opts?: { by?: "value" | "show" }): NameEntry<V> | undefined;
|
|
168
|
+
/** A named position's display: the one owning entry's name beside the shown value, or the value alone. */
|
|
169
|
+
export function nameOf<V>(entries: NameEntry<V>[], value: V, opts?: { by?: "value" | "show"; show?: (value: unknown) => string }): string;
|
package/src/index.mjs
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export { FormatError, CODES } from "./errors.mjs";
|
|
2
|
+
export { parseAmount, displayAmount } from "./amount.mjs";
|
|
3
|
+
export {
|
|
4
|
+
alignNow,
|
|
5
|
+
convertUnit,
|
|
6
|
+
displayDuration,
|
|
7
|
+
displayTimestamp,
|
|
8
|
+
parseDuration,
|
|
9
|
+
parseTimestamp,
|
|
10
|
+
} from "./time.mjs";
|
|
11
|
+
export { parseAccount, displayAccount } from "./account.mjs";
|
|
12
|
+
export { principalFromText, principalToText } from "./principal.mjs";
|
|
13
|
+
export { parseSubaccount, displaySubaccount } from "./subaccount.mjs";
|
|
14
|
+
export { parseBytes, displayBytes } from "./bytes.mjs";
|
|
15
|
+
export { describeInput, formatterFor, VALUE_TAGS } from "./formatter.mjs";
|
|
16
|
+
export { asUint8Array } from "./util.mjs";
|
|
17
|
+
export { normalizeName, resolveName, nameOf, ownerOf } from "./names.mjs";
|