diff --git a/README.md b/README.md index 4abb96b..bf4daaa 100644 --- a/README.md +++ b/README.md @@ -21,9 +21,10 @@ const re = parse('!re /fo./g', { customTags: [regexp] }) ## Available Types -- `regexp` (`!re`) - [RegExp] values, - using their default `/foo/flags` string representation. -- `sharedSymbol` (`!symbol/shared`) - [Shared Symbols], i.e. ones created with `Symbol.for()` +- `regexp` (`!re`) - [RegExp] values, using their default + `/foo/flags` string representation. +- `sharedSymbol` (`!symbol/shared`) - [Shared Symbols], i.e. ones + created with `Symbol.for()` - `symbol` (`!symbol`) - [Unique Symbols] - `nullobject` (`!nullobject) - Object with a `null` prototype - `error` (`!error`) - JavaScript [Error] objects @@ -31,6 +32,11 @@ const re = parse('!re /fo./g', { customTags: [regexp] }) - `functionTag` (`!function`) - JavaScript [Function] values (will also be used to stringify Class values, unless the `classTag` tag is loaded ahead of `functionTag`) +- `bigint` (`!bigint`) - JavaScript [BigInt] values. Note: in + order to use this effectively, a function must be provided as + `customTags` in order to prepend the `bigint` tag, or else the + built-in `!!int` tags will take priority. See + [bigint.test.ts](./src/bigint.test.ts) for examples. The function and class values created by parsing `!function` and `!class` tags will not actually replicate running code, but @@ -43,6 +49,7 @@ rather no-op function/class values with matching name and [Error]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error [Function]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions [Class]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes +[BigInt]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt ## Customising Tag Names diff --git a/src/bigint.test.ts b/src/bigint.test.ts new file mode 100644 index 0000000..20a1ae0 --- /dev/null +++ b/src/bigint.test.ts @@ -0,0 +1,109 @@ +import { test } from 'tap' +import { Document, parse, stringify, Tags, type Scalar } from 'yaml' + +import { bigint } from '.' + +const customTags = (tags: Tags) => ([bigint] as Tags).concat(tags) + +test('parse with n', t => { + const res: bigint = parse(`!bigint 123n`, { + customTags: [bigint] + }) + t.type(res, 'bigint') + t.equal(Number(res), 123) + t.equal(res, 123n) + t.end() +}) + +test('parse without n', t => { + const res: bigint = parse(`!bigint 123`, { customTags }) + t.type(res, 'bigint') + t.equal(Number(res), 123) + t.equal(res, 123n) + t.end() +}) + +test('negative zero is zero', t => { + const pos: bigint = parse(`!bigint 0`, { customTags }) + const neg: bigint = parse(`!bigint -0`, { customTags }) + t.equal(pos, 0n, 'pos equals zero') + t.equal(neg, 0n, 'neg equals zero') + t.equal(pos, neg, 'pos equals neg') + t.end() +}) + +test('parse hex, octal, binary', t => { + const cases = [ + '0b11011110101011011011111011101111', + '0B11011110101011011011111011101111', + '0b11011110101011011011111011101111n', + '0B11011110101011011011111011101111n', + '0o33653337357', + '0o33653337357n', + '3735928559', + '3735928559n', + '0xDeAdBeEf', + '0xDeAdBeEfn', + '0xDEADBEEF', + '0xDEADBEEFn', + '0XDEADBEEF', + '0XDEADBEEFn', + '0x0000DEADBEEF', + '0x0000DEADBEEFn', + '0xdeadbeef', + '0xdeadbeefn', + '0000000003735928559' + ] + for (const c of cases) { + const res: bigint = parse(`!bigint ${c}`, { customTags }) + t.equal(res, 0xdeadbeefn, `${c} value`) + t.type(res, 'bigint', `${c} typeof`) + } + t.end() +}) + +test('parse invalid', t => { + const opt = { customTags } + t.throws(() => parse('!bigint not a number\n', opt)) + t.throws(() => parse('!bigint 123.456\n', opt)) + t.throws(() => parse('!bigint 123x\n', opt)) + t.throws(() => parse('!bigint 0xBAD1DEAN\n', opt), 'n must be lowercase') + t.throws(() => parse('!bigint 0b012', opt), '2 is invalid binary digit') + t.throws(() => parse('!bigint 0o018', opt), '8 is invalid octal digit') + t.end() +}) + +test('empty string is 0n', t => { + t.equal(parse('!bigint ""', { customTags }), 0n) + t.end() +}) + +test('stringify', t => { + const doc = new Document(123n, { customTags }) + t.equal(doc.toString(), '!bigint 123n\n') + + doc.contents.value = Object(42n) + t.equal(doc.toString(), '!bigint 42n\n') + + doc.contents.value = 42 + t.throws(() => doc.toString(), { name: 'TypeError' }) + + t.equal( + stringify( + [0n, 123, 123n, BigInt('123'), Object(123n), Object(BigInt(123)), -123n], + { + customTags + } + ), + `- !bigint 0n +- 123 +- !bigint 123n +- !bigint 123n +- !bigint 123n +- !bigint 123n +- !bigint -123n +` + ) + + t.end() +}) diff --git a/src/bigint.ts b/src/bigint.ts new file mode 100644 index 0000000..b4fcf34 --- /dev/null +++ b/src/bigint.ts @@ -0,0 +1,26 @@ +import type { Scalar, ScalarTag } from 'yaml' +import { StringifyContext, stringifyString } from 'yaml/util' + +/** + * `!bigint` BigInt + * + * [BigInt](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt) values, + * using their conventional `123n` representation. + */ +export const bigint: ScalarTag = { + identify: (value: any) => { + return typeof value === 'bigint' || value instanceof BigInt + }, + tag: '!bigint', + resolve(str: string) { + if (str.endsWith('n')) str = str.substring(0, str.length - 1) + return BigInt(str) + }, + stringify(item: Scalar, ctx: StringifyContext, onComment, onChompKeep) { + if (!bigint.identify?.(item.value)) { + throw new TypeError(`${item.value} is not a bigint`) + } + const value = (item.value as BigInt).toString() + 'n' + return stringifyString({ value }, ctx, onComment, onChompKeep) + } +} diff --git a/src/index.ts b/src/index.ts index f750e4e..e31a889 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,3 +4,4 @@ export { nullobject } from './null-object.js' export { error } from './error.js' export { functionTag } from './function.js' export { classTag } from './class.js' +export { bigint } from './bigint.js'