roll-parser 3.0.0-alpha.0 → 3.0.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 +189 -0
- package/MIGRATION.md +147 -0
- package/README.md +985 -43
- package/dist/cli/args.d.ts +1 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +81 -0
- package/dist/cli/args.js.map +1 -0
- package/dist/cli/format.d.ts +16 -4
- package/dist/cli/format.d.ts.map +1 -1
- package/dist/cli/format.js +17 -0
- package/dist/cli/format.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +14 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/main.d.ts +36 -0
- package/dist/cli/main.d.ts.map +1 -0
- package/dist/cli/main.js +83 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/errors.d.ts +332 -14
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +141 -0
- package/dist/errors.js.map +1 -0
- package/dist/evaluator/die.d.ts +26 -0
- package/dist/evaluator/die.d.ts.map +1 -0
- package/dist/evaluator/die.js +19 -0
- package/dist/evaluator/die.js.map +1 -0
- package/dist/evaluator/env.d.ts +58 -0
- package/dist/evaluator/env.d.ts.map +1 -0
- package/dist/evaluator/env.js +11 -0
- package/dist/evaluator/env.js.map +1 -0
- package/dist/evaluator/evaluator.d.ts +62 -40
- package/dist/evaluator/evaluator.d.ts.map +1 -1
- package/dist/evaluator/evaluator.js +906 -0
- package/dist/evaluator/evaluator.js.map +1 -0
- package/dist/evaluator/modifiers/compare.d.ts +1 -1
- package/dist/evaluator/modifiers/compare.d.ts.map +1 -1
- package/dist/evaluator/modifiers/compare.js +15 -0
- package/dist/evaluator/modifiers/compare.js.map +1 -0
- package/dist/evaluator/modifiers/crit-threshold.d.ts +27 -0
- package/dist/evaluator/modifiers/crit-threshold.d.ts.map +1 -0
- package/dist/evaluator/modifiers/crit-threshold.js +23 -0
- package/dist/evaluator/modifiers/crit-threshold.js.map +1 -0
- package/dist/evaluator/modifiers/die-bound.d.ts +26 -0
- package/dist/evaluator/modifiers/die-bound.d.ts.map +1 -0
- package/dist/evaluator/modifiers/die-bound.js +14 -0
- package/dist/evaluator/modifiers/die-bound.js.map +1 -0
- package/dist/evaluator/modifiers/explode.d.ts +18 -6
- package/dist/evaluator/modifiers/explode.d.ts.map +1 -1
- package/dist/evaluator/modifiers/explode.js +103 -0
- package/dist/evaluator/modifiers/explode.js.map +1 -0
- package/dist/evaluator/modifiers/flags.d.ts +37 -0
- package/dist/evaluator/modifiers/flags.d.ts.map +1 -0
- package/dist/evaluator/modifiers/flags.js +18 -0
- package/dist/evaluator/modifiers/flags.js.map +1 -0
- package/dist/evaluator/modifiers/keep-drop.d.ts +12 -28
- package/dist/evaluator/modifiers/keep-drop.d.ts.map +1 -1
- package/dist/evaluator/modifiers/keep-drop.js +82 -0
- package/dist/evaluator/modifiers/keep-drop.js.map +1 -0
- package/dist/evaluator/modifiers/reroll.d.ts +14 -6
- package/dist/evaluator/modifiers/reroll.d.ts.map +1 -1
- package/dist/evaluator/modifiers/reroll.js +62 -0
- package/dist/evaluator/modifiers/reroll.js.map +1 -0
- package/dist/evaluator/modifiers/sort.d.ts +27 -0
- package/dist/evaluator/modifiers/sort.d.ts.map +1 -0
- package/dist/evaluator/modifiers/sort.js +13 -0
- package/dist/evaluator/modifiers/sort.js.map +1 -0
- package/dist/evaluator/modifiers/success-count.d.ts +2 -6
- package/dist/evaluator/modifiers/success-count.d.ts.map +1 -1
- package/dist/evaluator/modifiers/success-count.js +24 -0
- package/dist/evaluator/modifiers/success-count.js.map +1 -0
- package/dist/index.d.ts +35 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -1723
- package/dist/index.js.map +1 -0
- package/dist/lexer/lexer.d.ts +68 -8
- package/dist/lexer/lexer.d.ts.map +1 -1
- package/dist/lexer/lexer.js +260 -0
- package/dist/lexer/lexer.js.map +1 -0
- package/dist/lexer/tokens.d.ts +52 -7
- package/dist/lexer/tokens.d.ts.map +1 -1
- package/dist/lexer/tokens.js +42 -0
- package/dist/lexer/tokens.js.map +1 -0
- package/dist/parser/ast.d.ts +419 -80
- package/dist/parser/ast.d.ts.map +1 -1
- package/dist/parser/ast.js +52 -0
- package/dist/parser/ast.js.map +1 -0
- package/dist/parser/guards.d.ts +106 -0
- package/dist/parser/guards.d.ts.map +1 -0
- package/dist/parser/guards.js +121 -0
- package/dist/parser/guards.js.map +1 -0
- package/dist/parser/parser.d.ts +162 -15
- package/dist/parser/parser.d.ts.map +1 -1
- package/dist/parser/parser.js +751 -0
- package/dist/parser/parser.js.map +1 -0
- package/dist/rng/mock.d.ts +74 -13
- package/dist/rng/mock.d.ts.map +1 -1
- package/dist/rng/mock.js +30 -0
- package/dist/rng/mock.js.map +1 -0
- package/dist/rng/seeded.d.ts +149 -10
- package/dist/rng/seeded.d.ts.map +1 -1
- package/dist/rng/seeded.js +138 -0
- package/dist/rng/seeded.js.map +1 -0
- package/dist/rng/types.d.ts +57 -0
- package/dist/rng/types.d.ts.map +1 -1
- package/dist/rng/types.js +2 -0
- package/dist/rng/types.js.map +1 -0
- package/dist/roll.d.ts +59 -25
- package/dist/roll.d.ts.map +1 -1
- package/dist/roll.js +8 -0
- package/dist/roll.js.map +1 -0
- package/dist/testing.d.ts +5 -4
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +2 -38
- package/dist/testing.js.map +1 -0
- package/dist/types.d.ts +427 -24
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +2 -0
- package/dist/version.js.map +1 -0
- package/package.json +93 -40
- package/src/cli/args.ts +66 -9
- package/src/cli/format.ts +30 -7
- package/src/cli/index.ts +27 -67
- package/src/cli/main.ts +129 -0
- package/src/errors.ts +488 -23
- package/src/evaluator/die.ts +50 -0
- package/src/evaluator/env.ts +73 -0
- package/src/evaluator/evaluator.ts +1296 -349
- package/src/evaluator/modifiers/compare.ts +1 -1
- package/src/evaluator/modifiers/crit-threshold.ts +56 -0
- package/src/evaluator/modifiers/die-bound.ts +39 -0
- package/src/evaluator/modifiers/explode.ts +82 -76
- package/src/evaluator/modifiers/flags.ts +61 -0
- package/src/evaluator/modifiers/keep-drop.ts +124 -126
- package/src/evaluator/modifiers/reroll.ts +36 -64
- package/src/evaluator/modifiers/sort.ts +43 -0
- package/src/evaluator/modifiers/success-count.ts +6 -9
- package/src/index.ts +73 -34
- package/src/lexer/lexer.ts +201 -35
- package/src/lexer/tokens.ts +72 -7
- package/src/parser/ast.ts +453 -104
- package/src/parser/guards.ts +248 -0
- package/src/parser/parser.ts +835 -135
- package/src/rng/mock.ts +75 -14
- package/src/rng/seeded.ts +323 -58
- package/src/rng/types.ts +57 -0
- package/src/roll.ts +66 -41
- package/src/testing.ts +5 -9
- package/src/types.ts +416 -24
- package/src/version.ts +2 -0
- package/dist/cli.js +0 -1775
- package/dist/evaluator/index.d.ts +0 -8
- package/dist/evaluator/index.d.ts.map +0 -1
- package/dist/index.mjs +0 -1724
- package/dist/rng/index.d.ts +0 -8
- package/dist/rng/index.d.ts.map +0 -1
- package/dist/testing.mjs +0 -39
- package/src/evaluator/index.ts +0 -14
- package/src/rng/index.ts +0 -8
package/package.json
CHANGED
|
@@ -1,39 +1,50 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "roll-parser",
|
|
3
|
-
"version": "3.0.0
|
|
4
|
-
"description": "High-performance dice notation parser for tabletop RPGs. TypeScript-first, Bun
|
|
3
|
+
"version": "3.0.0",
|
|
4
|
+
"description": "High-performance dice notation parser for tabletop RPGs. TypeScript-first, runs on Node, Bun, and browsers with zero dependencies.",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
"
|
|
10
|
-
"sideEffects": false,
|
|
6
|
+
"bin": {
|
|
7
|
+
"roll-parser": "./dist/cli/index.js"
|
|
8
|
+
},
|
|
9
|
+
"sideEffects": ["./dist/cli/index.js"],
|
|
11
10
|
"exports": {
|
|
12
11
|
".": {
|
|
13
12
|
"types": "./dist/index.d.ts",
|
|
14
|
-
"
|
|
15
|
-
"require": "./dist/index.js"
|
|
13
|
+
"default": "./dist/index.js"
|
|
16
14
|
},
|
|
17
15
|
"./testing": {
|
|
18
16
|
"types": "./dist/testing.d.ts",
|
|
19
|
-
"
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
"default": "./dist/testing.js"
|
|
18
|
+
},
|
|
19
|
+
"./package.json": "./package.json"
|
|
22
20
|
},
|
|
21
|
+
"unpkg": "./dist/index.js",
|
|
22
|
+
"jsdelivr": "./dist/index.js",
|
|
23
23
|
"files": [
|
|
24
24
|
"dist",
|
|
25
25
|
"src",
|
|
26
|
-
"
|
|
26
|
+
"CHANGELOG.md",
|
|
27
|
+
"MIGRATION.md",
|
|
28
|
+
"!src/**/*.test.ts",
|
|
29
|
+
"!src/test-helpers.ts"
|
|
27
30
|
],
|
|
28
31
|
"keywords": [
|
|
29
32
|
"parser",
|
|
30
33
|
"dice",
|
|
34
|
+
"dice-notation",
|
|
35
|
+
"dice-roller",
|
|
36
|
+
"dice-parser",
|
|
31
37
|
"roll",
|
|
32
38
|
"rpg",
|
|
39
|
+
"ttrpg",
|
|
40
|
+
"tabletop",
|
|
33
41
|
"dnd",
|
|
34
42
|
"d20",
|
|
35
|
-
"pratt-parser",
|
|
36
43
|
"typescript",
|
|
44
|
+
"esm",
|
|
45
|
+
"browser",
|
|
46
|
+
"zero-dependency",
|
|
47
|
+
"cli",
|
|
37
48
|
"bun"
|
|
38
49
|
],
|
|
39
50
|
"homepage": "https://github.com/edloidas/roll-parser#readme",
|
|
@@ -41,7 +52,7 @@
|
|
|
41
52
|
"url": "https://github.com/edloidas/roll-parser/issues"
|
|
42
53
|
},
|
|
43
54
|
"license": "MIT",
|
|
44
|
-
"author": "Mikita Taukachou <edloidas@gmail.com> (https://edloidas.
|
|
55
|
+
"author": "Mikita Taukachou <edloidas@gmail.com> (https://edloidas.io)",
|
|
45
56
|
"repository": {
|
|
46
57
|
"type": "git",
|
|
47
58
|
"url": "git+https://github.com/edloidas/roll-parser.git"
|
|
@@ -49,40 +60,82 @@
|
|
|
49
60
|
"publishConfig": {
|
|
50
61
|
"access": "public"
|
|
51
62
|
},
|
|
63
|
+
"workspaces": ["scripts/browser-smoke", "scripts/docs"],
|
|
52
64
|
"scripts": {
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"format:check": "biome format .",
|
|
58
|
-
"check": "bun typecheck && bun lint && bun format:check",
|
|
59
|
-
"check:fix": "bun typecheck && bun lint:fix && bun format",
|
|
65
|
+
"prepare": "git rev-parse --git-dir > /dev/null 2>&1 && git config core.hooksPath .githooks || exit 0",
|
|
66
|
+
"typecheck": "tsc --noEmit && bun run build && tsc --noEmit -p site/tsconfig.json && tsc --noEmit -p scripts/tsconfig.json && tsc --noEmit -p bench/tsconfig.json",
|
|
67
|
+
"check": "bun typecheck && biome check .",
|
|
68
|
+
"check:fix": "bun typecheck && biome check --write .",
|
|
60
69
|
"check:version": "bun scripts/check-version.ts",
|
|
61
70
|
"check:changelog": "bun scripts/check-changelog.ts",
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
"build
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
-
"
|
|
68
|
-
"
|
|
69
|
-
"
|
|
71
|
+
"generate:version": "bun scripts/generate-version.ts",
|
|
72
|
+
"clean": "rm -rf dist",
|
|
73
|
+
"build": "bun scripts/build.ts",
|
|
74
|
+
"check:package": "attw --pack . --profile esm-only && publint",
|
|
75
|
+
"check:size": "size-limit",
|
|
76
|
+
"bench": "bun run bench/index.bench.ts",
|
|
77
|
+
"bench:lex": "bun run bench/lex.bench.ts",
|
|
78
|
+
"bench:parse": "bun run bench/parse.bench.ts",
|
|
79
|
+
"bench:evaluate": "bun run bench/evaluate.bench.ts",
|
|
80
|
+
"bench:roll": "bun run bench/roll.bench.ts",
|
|
81
|
+
"bench:json": "bun run scripts/bench-json.ts",
|
|
82
|
+
"bench:competitors": "cd bench/competitors && bun install && bun run all",
|
|
83
|
+
"site:build": "bun run build && bun scripts/build-site.ts",
|
|
84
|
+
"site:check": "bun scripts/check-site.ts",
|
|
85
|
+
"site:dev": "bun run build && bun ./site/index.html ./site/reference.html",
|
|
86
|
+
"site:preview": "bun scripts/serve-site.ts",
|
|
70
87
|
"test": "bun test",
|
|
71
|
-
"test:node": "node -e \"import('./dist/index.mjs').then(m => { const r = m.roll('1d6'); if (!(r.total >= 1 && r.total <= 6)) throw new Error('ESM smoke failed: total=' + r.total); console.log('Node.js ESM smoke: ok'); })\" && node -e \"const m = require('./dist/index.js'); const r = m.roll('1d6'); if (!(r.total >= 1 && r.total <= 6)) throw new Error('CJS smoke failed: total=' + r.total); console.log('Node.js CJS smoke: ok');\"",
|
|
72
88
|
"test:watch": "bun test --watch",
|
|
73
|
-
"test:ci": "bun test --bail",
|
|
74
|
-
"validate": "bun run check && bun run build && bun test:ci",
|
|
75
|
-
"
|
|
76
|
-
"release:dry": "bun run check:version && bun run check:changelog && bun run check && bun run build && bun test",
|
|
89
|
+
"test:ci": "bun test --bail --coverage",
|
|
90
|
+
"validate": "bun run check && bun run build && bun run check:package && bun run check:size && bun run site:build && bun run site:check && bun test:ci",
|
|
91
|
+
"release:dry": "bun run check:version && bun run check:changelog && bun audit --audit-level=high && bun run clean && bun run validate",
|
|
77
92
|
"prepublishOnly": "bun run release:dry"
|
|
78
93
|
},
|
|
94
|
+
"nano-staged": {
|
|
95
|
+
"*.{ts,tsx}": "biome check --write --no-errors-on-unmatched"
|
|
96
|
+
},
|
|
97
|
+
"size-limit": [
|
|
98
|
+
{
|
|
99
|
+
"name": "index.js (full library)",
|
|
100
|
+
"path": "dist/index.js",
|
|
101
|
+
"limit": "12.5 kB"
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"name": "index.js — { parse }",
|
|
105
|
+
"path": "dist/index.js",
|
|
106
|
+
"import": "{ parse }",
|
|
107
|
+
"limit": "6 kB"
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"name": "index.js — { roll }",
|
|
111
|
+
"path": "dist/index.js",
|
|
112
|
+
"import": "{ roll }",
|
|
113
|
+
"limit": "12 kB"
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"name": "testing.js — { createMockRng }",
|
|
117
|
+
"path": "dist/testing.js",
|
|
118
|
+
"import": "{ createMockRng }",
|
|
119
|
+
"limit": "250 B"
|
|
120
|
+
}
|
|
121
|
+
],
|
|
79
122
|
"devDependencies": {
|
|
80
|
-
"@
|
|
81
|
-
"@
|
|
82
|
-
"
|
|
83
|
-
"
|
|
123
|
+
"@arethetypeswrong/cli": "^0.18.5",
|
|
124
|
+
"@biomejs/biome": "^2.5.6",
|
|
125
|
+
"@size-limit/preset-small-lib": "^13.0.2",
|
|
126
|
+
"@types/bun": "^1.3.14",
|
|
127
|
+
"esbuild": "^0.28.1",
|
|
128
|
+
"fast-check": "^4.9.0",
|
|
129
|
+
"mitata": "^1.0.34",
|
|
130
|
+
"nano-staged": "^1.0.2",
|
|
131
|
+
"publint": "^0.3.22",
|
|
132
|
+
"size-limit": "^13.0.2",
|
|
133
|
+
"typescript": "^7.0.2"
|
|
134
|
+
},
|
|
135
|
+
"overrides": {
|
|
136
|
+
"brace-expansion": "^5.0.9"
|
|
84
137
|
},
|
|
85
138
|
"engines": {
|
|
86
|
-
"node": ">=
|
|
139
|
+
"node": ">=22.12.0"
|
|
87
140
|
}
|
|
88
141
|
}
|
package/src/cli/args.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
export type CliArgs = {
|
|
13
13
|
notation: string | undefined;
|
|
14
14
|
verbose: boolean;
|
|
15
|
+
json: boolean;
|
|
15
16
|
seed: string | undefined;
|
|
16
17
|
showHelp: boolean;
|
|
17
18
|
showVersion: boolean;
|
|
@@ -22,6 +23,46 @@ export type CliArgs = {
|
|
|
22
23
|
*/
|
|
23
24
|
export type ParseArgsResult = { ok: true; args: CliArgs } | { ok: false; error: string };
|
|
24
25
|
|
|
26
|
+
/** Argument that stops option parsing — everything after it is notation. */
|
|
27
|
+
const TERMINATOR = '--';
|
|
28
|
+
|
|
29
|
+
/** Defaults every parse starts from. */
|
|
30
|
+
const BASE_ARGS: CliArgs = {
|
|
31
|
+
notation: undefined,
|
|
32
|
+
verbose: false,
|
|
33
|
+
json: false,
|
|
34
|
+
seed: undefined,
|
|
35
|
+
showHelp: false,
|
|
36
|
+
showVersion: false,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Finds `--help` / `--version` anywhere before the `--` terminator. Help wins
|
|
41
|
+
* over version regardless of order, and both win over usage errors — a user
|
|
42
|
+
* who mistyped an option is asking for the manual, not for a diagnostic.
|
|
43
|
+
*/
|
|
44
|
+
function findInformationalFlag(argv: string[]): 'help' | 'version' | undefined {
|
|
45
|
+
let flag: 'help' | 'version' | undefined;
|
|
46
|
+
|
|
47
|
+
for (const arg of argv) {
|
|
48
|
+
if (arg === TERMINATOR) break;
|
|
49
|
+
if (arg === '--help' || arg === '-h') return 'help';
|
|
50
|
+
if (arg === '--version') flag = 'version';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return flag;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* True for an argument that starts with `-` yet reads as notation rather than
|
|
58
|
+
* an option: negative numbers (`-3`) and negative-prefixed expressions
|
|
59
|
+
* (`-d6`, `-D6`, `-dF`, `-(2d6)`, `-{2d6}`, `-@str`). A fallback for users who
|
|
60
|
+
* do not reach for `--`; `--` remains the unambiguous form.
|
|
61
|
+
*/
|
|
62
|
+
function isNegativeNotation(arg: string): boolean {
|
|
63
|
+
return arg.length > 1 && /^[\ddD({@]/.test(arg.slice(1));
|
|
64
|
+
}
|
|
65
|
+
|
|
25
66
|
/**
|
|
26
67
|
* Parses a raw argument array into typed CLI options.
|
|
27
68
|
*
|
|
@@ -29,24 +70,38 @@ export type ParseArgsResult = { ok: true; args: CliArgs } | { ok: false; error:
|
|
|
29
70
|
* @returns Parsed result or an error message for usage errors
|
|
30
71
|
*/
|
|
31
72
|
export function parseArgs(argv: string[]): ParseArgsResult {
|
|
73
|
+
const informational = findInformationalFlag(argv);
|
|
74
|
+
if (informational != null) {
|
|
75
|
+
return {
|
|
76
|
+
ok: true,
|
|
77
|
+
args: {
|
|
78
|
+
...BASE_ARGS,
|
|
79
|
+
showHelp: informational === 'help',
|
|
80
|
+
showVersion: informational === 'version',
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
32
85
|
let verbose = false;
|
|
86
|
+
let json = false;
|
|
33
87
|
let seed: string | undefined;
|
|
34
|
-
let showHelp = false;
|
|
35
|
-
let showVersion = false;
|
|
36
88
|
const positional: string[] = [];
|
|
37
89
|
|
|
38
90
|
for (let i = 0; i < argv.length; i++) {
|
|
39
91
|
const arg = argv[i] as string;
|
|
40
92
|
|
|
41
|
-
if (arg ===
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
showVersion = true;
|
|
93
|
+
if (arg === TERMINATOR) {
|
|
94
|
+
positional.push(...argv.slice(i + 1));
|
|
95
|
+
break;
|
|
45
96
|
} else if (arg === '--verbose' || arg === '-v') {
|
|
46
97
|
verbose = true;
|
|
98
|
+
} else if (arg === '--json') {
|
|
99
|
+
json = true;
|
|
47
100
|
} else if (arg === '--seed') {
|
|
101
|
+
// A seed is an opaque string, so any non-empty next argument counts:
|
|
102
|
+
// `--seed -abc` is a valid seed, not a missing value.
|
|
48
103
|
const next = argv[i + 1];
|
|
49
|
-
if (next
|
|
104
|
+
if (next == null || next === '') {
|
|
50
105
|
return { ok: false, error: 'Missing value for --seed' };
|
|
51
106
|
}
|
|
52
107
|
seed = next;
|
|
@@ -59,14 +114,16 @@ export function parseArgs(argv: string[]): ParseArgsResult {
|
|
|
59
114
|
seed = value;
|
|
60
115
|
} else if (arg.startsWith('--')) {
|
|
61
116
|
return { ok: false, error: `Unknown option: ${arg}` };
|
|
62
|
-
} else if (arg.startsWith('-') &&
|
|
117
|
+
} else if (arg.startsWith('-') && !isNegativeNotation(arg)) {
|
|
63
118
|
return { ok: false, error: `Unknown option: ${arg}` };
|
|
64
119
|
} else {
|
|
65
120
|
positional.push(arg);
|
|
66
121
|
}
|
|
67
122
|
}
|
|
68
123
|
|
|
124
|
+
// Joined, not separate rolls — a shell splits `roll-parser 2d6 + 3` into three
|
|
125
|
+
// words and the user means one expression.
|
|
69
126
|
const notation = positional.length > 0 ? positional.join(' ') : undefined;
|
|
70
127
|
|
|
71
|
-
return { ok: true, args: { notation, verbose,
|
|
128
|
+
return { ok: true, args: { ...BASE_ARGS, notation, verbose, json, seed } };
|
|
72
129
|
}
|
package/src/cli/format.ts
CHANGED
|
@@ -6,19 +6,38 @@
|
|
|
6
6
|
* @module cli/format
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import type { RollResult } from '../types';
|
|
9
|
+
import type { RollResult } from '../types.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Output mode selectors for {@link formatResult}.
|
|
13
|
+
*/
|
|
14
|
+
export type FormatOptions = {
|
|
15
|
+
/** Emit the whole result as compact JSON. Takes precedence over `verbose`. */
|
|
16
|
+
json?: boolean;
|
|
17
|
+
/** Show the detailed roll breakdown instead of the bare total. */
|
|
18
|
+
verbose?: boolean;
|
|
19
|
+
};
|
|
10
20
|
|
|
11
21
|
/**
|
|
12
22
|
* Formats a roll result for terminal display.
|
|
13
23
|
*
|
|
14
24
|
* In normal mode, returns just the total. In verbose mode, returns the
|
|
15
|
-
* rendered breakdown with terminal-safe formatting for dropped dice.
|
|
25
|
+
* rendered breakdown with terminal-safe formatting for dropped dice. In JSON
|
|
26
|
+
* mode, returns the complete `RollResult` — including the structured `parts`
|
|
27
|
+
* tree — as a single-line `JSON.stringify` payload, where `degree` appears as
|
|
28
|
+
* its numeric `DegreeOfSuccess` value.
|
|
16
29
|
*
|
|
17
30
|
* @param result - The roll result to format
|
|
18
|
-
* @param
|
|
31
|
+
* @param options - Output mode; JSON wins when combined with verbose
|
|
19
32
|
* @returns Formatted string for terminal output
|
|
20
33
|
*/
|
|
21
|
-
export function formatResult(result: RollResult,
|
|
34
|
+
export function formatResult(result: RollResult, options: FormatOptions = {}): string {
|
|
35
|
+
const { json = false, verbose = false } = options;
|
|
36
|
+
|
|
37
|
+
if (json) {
|
|
38
|
+
return JSON.stringify(result);
|
|
39
|
+
}
|
|
40
|
+
|
|
22
41
|
if (!verbose) {
|
|
23
42
|
return String(result.total);
|
|
24
43
|
}
|
|
@@ -30,16 +49,20 @@ export function formatResult(result: RollResult, verbose: boolean): string {
|
|
|
30
49
|
* Converts markdown-style dice markers to terminal-friendly forms.
|
|
31
50
|
*
|
|
32
51
|
* The evaluator uses markdown syntax in the rendered field:
|
|
33
|
-
* `~~value~~` — dropped dice
|
|
52
|
+
* `~~value~~` — dropped dice or dropped group sub-rolls
|
|
34
53
|
* `**value**` — dice counted as success
|
|
35
54
|
* `__value__` — dice counted as failure
|
|
36
55
|
*
|
|
37
56
|
* For plain terminals these become `(value)`, `[value]`, and `{value}` so
|
|
38
57
|
* the per-die classification stays visible without any markup dependency.
|
|
58
|
+
* Dropped spans can wrap a whole sub-roll (e.g. `~~1d8[2]~~` from
|
|
59
|
+
* `{1d8, 1d10}kh1`), so the strikethrough pattern accepts any tilde-free
|
|
60
|
+
* content, not just a single number. The evaluator never nests `~~`
|
|
61
|
+
* (see `stripInnerMarkers`), so the tilde-free span match is safe.
|
|
39
62
|
*/
|
|
40
63
|
function formatRendered(rendered: string): string {
|
|
41
64
|
return rendered
|
|
42
|
-
.replace(/~~(-?\d+)~~/g, '($1)')
|
|
43
65
|
.replace(/\*\*(-?\d+)\*\*/g, '[$1]')
|
|
44
|
-
.replace(/__(-?\d+)__/g, '{$1}')
|
|
66
|
+
.replace(/__(-?\d+)__/g, '{$1}')
|
|
67
|
+
.replace(/~~([^~]+)~~/g, '($1)');
|
|
45
68
|
}
|
package/src/cli/index.ts
CHANGED
|
@@ -3,73 +3,33 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* CLI entry point for roll-parser.
|
|
5
5
|
*
|
|
6
|
+
* Owns the process-level wiring only; the command itself lives in
|
|
7
|
+
* `./main.js` so it stays testable without spawning a subprocess.
|
|
8
|
+
*
|
|
6
9
|
* @module cli/index
|
|
7
10
|
*/
|
|
8
11
|
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
if (!parsed.ok) {
|
|
35
|
-
process.stderr.write(`Error: ${parsed.error}\n`);
|
|
36
|
-
process.stderr.write('Run "roll-parser --help" for usage.\n');
|
|
37
|
-
process.exitCode = 2;
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const { args } = parsed;
|
|
42
|
-
|
|
43
|
-
if (args.showHelp) {
|
|
44
|
-
process.stdout.write(HELP_TEXT);
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
if (args.showVersion) {
|
|
49
|
-
process.stdout.write(`${VERSION}\n`);
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (args.notation == null) {
|
|
54
|
-
process.stderr.write('Error: No dice notation provided.\n');
|
|
55
|
-
process.stderr.write('Run "roll-parser --help" for usage.\n');
|
|
56
|
-
process.exitCode = 2;
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
try {
|
|
61
|
-
const options = args.seed != null ? { seed: args.seed } : {};
|
|
62
|
-
const result = roll(args.notation, options);
|
|
63
|
-
const output = formatResult(result, args.verbose);
|
|
64
|
-
process.stdout.write(`${output}\n`);
|
|
65
|
-
} catch (error) {
|
|
66
|
-
if (isRollParserError(error)) {
|
|
67
|
-
process.stderr.write(`Error: ${error.message}\n`);
|
|
68
|
-
process.exitCode = 1;
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
throw error;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
main();
|
|
12
|
+
import { main } from './main.js';
|
|
13
|
+
|
|
14
|
+
// Declared locally because the library build sets `types: []` — runtime globals
|
|
15
|
+
// are type errors everywhere, and this is the one place `process` is legitimate.
|
|
16
|
+
declare const process: {
|
|
17
|
+
argv: string[];
|
|
18
|
+
exitCode: number | undefined;
|
|
19
|
+
stdout: { write(text: string): void };
|
|
20
|
+
stderr: { write(text: string): void };
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const exitCode = main({
|
|
24
|
+
argv: process.argv.slice(2),
|
|
25
|
+
stdout: (text) => {
|
|
26
|
+
process.stdout.write(text);
|
|
27
|
+
},
|
|
28
|
+
stderr: (text) => {
|
|
29
|
+
process.stderr.write(text);
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Left unset on success — assigning 0 would clobber an exit code a surrounding
|
|
34
|
+
// runtime hook may already have set.
|
|
35
|
+
if (exitCode !== 0) process.exitCode = exitCode;
|
package/src/cli/main.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI command implementation for roll-parser.
|
|
3
|
+
*
|
|
4
|
+
* Split out of `index.ts` so `main` and `writeErrorContext` can run
|
|
5
|
+
* in-process under the test runner — the shebang entry point stays a thin
|
|
6
|
+
* wrapper that owns `process.argv`, the real streams, and `process.exitCode`.
|
|
7
|
+
*
|
|
8
|
+
* @module cli/main
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { getErrorSpan, isRollParserError } from '../errors.js';
|
|
12
|
+
import { VERSION } from '../index.js';
|
|
13
|
+
import { roll } from '../roll.js';
|
|
14
|
+
import { parseArgs } from './args.js';
|
|
15
|
+
import { formatResult } from './format.js';
|
|
16
|
+
|
|
17
|
+
const HELP_TEXT = `roll-parser v${VERSION}
|
|
18
|
+
|
|
19
|
+
Usage: roll-parser [options] [--] <notation>
|
|
20
|
+
|
|
21
|
+
Options:
|
|
22
|
+
-h, --help Show this help message
|
|
23
|
+
--version Show version number
|
|
24
|
+
-v, --verbose Show detailed roll breakdown
|
|
25
|
+
--json Print the whole result as compact JSON (wins over --verbose)
|
|
26
|
+
--seed <value> Use seed for reproducible rolls
|
|
27
|
+
-- Treat every following argument as notation
|
|
28
|
+
|
|
29
|
+
JSON output:
|
|
30
|
+
Emits the complete result, including the structured "parts" tree. The
|
|
31
|
+
"degree" field (DegreeOfSuccess) serializes as a number: 0 critical failure,
|
|
32
|
+
1 failure, 2 success, 3 critical success. Errors stay plain text on stderr.
|
|
33
|
+
|
|
34
|
+
Exit codes:
|
|
35
|
+
0 Success
|
|
36
|
+
1 Roll or parse error
|
|
37
|
+
2 Usage error
|
|
38
|
+
|
|
39
|
+
Examples:
|
|
40
|
+
roll-parser 2d6+3
|
|
41
|
+
roll-parser 4d6kh3 --verbose
|
|
42
|
+
roll-parser 4d6dl1 --seed "character-str"
|
|
43
|
+
roll-parser "1d20+7 vs 25" --json
|
|
44
|
+
roll-parser -- -1d6+3
|
|
45
|
+
`;
|
|
46
|
+
|
|
47
|
+
/** Sink for one stream's worth of CLI output. */
|
|
48
|
+
export type WriteFn = (text: string) => void;
|
|
49
|
+
|
|
50
|
+
/** Everything `main` needs from the host process. */
|
|
51
|
+
export type CliEnv = {
|
|
52
|
+
/** Arguments after the interpreter and script path (`process.argv.slice(2)`). */
|
|
53
|
+
argv: string[];
|
|
54
|
+
stdout: WriteFn;
|
|
55
|
+
stderr: WriteFn;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Prints the notation with a caret under the error position. `getErrorSpan`
|
|
60
|
+
* normalizes the lexer/parser `position` and the evaluator `start`/`end`
|
|
61
|
+
* shapes, so no duck-typing is needed here. Skipped for multi-line notations
|
|
62
|
+
* and out-of-range positions to keep the caret honest.
|
|
63
|
+
*/
|
|
64
|
+
export function writeErrorContext(notation: string, error: unknown, write: WriteFn): void {
|
|
65
|
+
const span = getErrorSpan(error);
|
|
66
|
+
if (span == null) return;
|
|
67
|
+
if (notation.includes('\n') || span.start > notation.length) return;
|
|
68
|
+
|
|
69
|
+
// Code points, not UTF-16 units — an astral character ('🎲') is two units but
|
|
70
|
+
// one column, and counting units shifts the caret right.
|
|
71
|
+
const column = [...notation.slice(0, span.start)].length;
|
|
72
|
+
|
|
73
|
+
write(` ${notation}\n`);
|
|
74
|
+
write(` ${' '.repeat(column)}^\n`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Runs one CLI invocation and returns the process exit code: `0` on success,
|
|
79
|
+
* `1` for a roll-parser error, `2` for a usage error. Anything that is not a
|
|
80
|
+
* `RollParserError` propagates so the runtime reports it with a stack.
|
|
81
|
+
*
|
|
82
|
+
* `--json` swaps the success payload only — diagnostics stay plain text on
|
|
83
|
+
* stderr and the exit codes are identical, so scripts can branch on the code
|
|
84
|
+
* before parsing stdout.
|
|
85
|
+
*/
|
|
86
|
+
export function main(env: CliEnv): number {
|
|
87
|
+
const { argv, stdout, stderr } = env;
|
|
88
|
+
const parsed = parseArgs(argv);
|
|
89
|
+
|
|
90
|
+
if (!parsed.ok) {
|
|
91
|
+
stderr(`Error: ${parsed.error}\n`);
|
|
92
|
+
stderr('Run "roll-parser --help" for usage.\n');
|
|
93
|
+
return 2;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const { args } = parsed;
|
|
97
|
+
|
|
98
|
+
if (args.showHelp) {
|
|
99
|
+
stdout(HELP_TEXT);
|
|
100
|
+
return 0;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (args.showVersion) {
|
|
104
|
+
stdout(`${VERSION}\n`);
|
|
105
|
+
return 0;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (args.notation == null) {
|
|
109
|
+
stderr('Error: No dice notation provided.\n');
|
|
110
|
+
stderr('Run "roll-parser --help" for usage.\n');
|
|
111
|
+
return 2;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const options = args.seed != null ? { seed: args.seed } : {};
|
|
116
|
+
const result = roll(args.notation, options);
|
|
117
|
+
const output = formatResult(result, { json: args.json, verbose: args.verbose });
|
|
118
|
+
stdout(`${output}\n`);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
if (isRollParserError(error)) {
|
|
121
|
+
stderr(`Error: ${error.message}\n`);
|
|
122
|
+
writeErrorContext(args.notation, error, stderr);
|
|
123
|
+
return 1;
|
|
124
|
+
}
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return 0;
|
|
129
|
+
}
|