roll-parser 3.0.0-beta.0 → 3.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 +142 -9
- package/MIGRATION.md +219 -0
- package/README.md +1026 -154
- 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 +15 -3
- package/dist/cli/format.d.ts.map +1 -1
- package/dist/cli/format.js +18 -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 -17
- 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 +27 -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 +89 -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 +41 -75
- package/dist/evaluator/evaluator.d.ts.map +1 -1
- package/dist/evaluator/evaluator.js +914 -0
- package/dist/evaluator/evaluator.js.map +1 -0
- 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 +57 -5
- package/dist/evaluator/modifiers/crit-threshold.d.ts.map +1 -1
- package/dist/evaluator/modifiers/crit-threshold.js +42 -0
- package/dist/evaluator/modifiers/crit-threshold.js.map +1 -0
- package/dist/evaluator/modifiers/die-bound.d.ts +29 -0
- package/dist/evaluator/modifiers/die-bound.d.ts.map +1 -0
- package/dist/evaluator/modifiers/die-bound.js +17 -0
- package/dist/evaluator/modifiers/die-bound.js.map +1 -0
- package/dist/evaluator/modifiers/explode.d.ts +13 -5
- package/dist/evaluator/modifiers/explode.d.ts.map +1 -1
- package/dist/evaluator/modifiers/explode.js +105 -0
- package/dist/evaluator/modifiers/explode.js.map +1 -0
- package/dist/evaluator/modifiers/flags.d.ts +47 -0
- package/dist/evaluator/modifiers/flags.d.ts.map +1 -0
- package/dist/evaluator/modifiers/flags.js +25 -0
- package/dist/evaluator/modifiers/flags.js.map +1 -0
- package/dist/evaluator/modifiers/keep-drop.d.ts +15 -29
- 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 +12 -4
- package/dist/evaluator/modifiers/reroll.d.ts.map +1 -1
- package/dist/evaluator/modifiers/reroll.js +68 -0
- package/dist/evaluator/modifiers/reroll.js.map +1 -0
- package/dist/evaluator/modifiers/sort.d.ts +5 -1
- package/dist/evaluator/modifiers/sort.d.ts.map +1 -1
- 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 +6 -7
- package/dist/evaluator/modifiers/success-count.d.ts.map +1 -1
- package/dist/evaluator/modifiers/success-count.js +25 -0
- package/dist/evaluator/modifiers/success-count.js.map +1 -0
- package/dist/index.d.ts +33 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -2560
- package/dist/index.js.map +1 -26
- package/dist/lexer/lexer.d.ts +50 -5
- 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 +31 -6
- 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 +324 -165
- 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 +118 -14
- 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/render.d.ts +95 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +227 -0
- package/dist/render.js.map +1 -0
- package/dist/rng/mock.d.ts +73 -12
- 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 +141 -9
- 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 +58 -28
- 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 -41
- package/dist/testing.js.map +1 -11
- package/dist/types.d.ts +349 -47
- 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 -34
- package/src/cli/args.ts +66 -10
- package/src/cli/format.ts +37 -26
- package/src/cli/index.ts +27 -84
- package/src/cli/main.ts +129 -0
- package/src/errors.ts +480 -27
- package/src/evaluator/die.ts +51 -0
- package/src/evaluator/env.ts +105 -0
- package/src/evaluator/evaluator.ts +693 -434
- package/src/evaluator/modifiers/crit-threshold.ts +96 -14
- package/src/evaluator/modifiers/die-bound.ts +48 -0
- package/src/evaluator/modifiers/explode.ts +70 -62
- package/src/evaluator/modifiers/flags.ts +78 -0
- package/src/evaluator/modifiers/keep-drop.ts +129 -127
- package/src/evaluator/modifiers/reroll.ts +44 -56
- package/src/evaluator/modifiers/sort.ts +21 -2
- package/src/evaluator/modifiers/success-count.ts +24 -12
- package/src/index.ts +56 -35
- package/src/lexer/lexer.ts +107 -34
- package/src/lexer/tokens.ts +31 -6
- package/src/parser/ast.ts +333 -346
- package/src/parser/guards.ts +248 -0
- package/src/parser/parser.ts +419 -242
- package/src/render.ts +392 -0
- package/src/rng/mock.ts +74 -13
- package/src/rng/seeded.ts +299 -64
- package/src/rng/types.ts +57 -0
- package/src/roll.ts +64 -47
- package/src/testing.ts +5 -9
- package/src/types.ts +353 -46
- package/src/version.ts +2 -0
- package/dist/cli.js +0 -2608
- package/dist/cli.js.map +0 -28
- package/dist/evaluator/index.d.ts +0 -8
- package/dist/evaluator/index.d.ts.map +0 -1
- package/dist/rng/index.d.ts +0 -8
- package/dist/rng/index.d.ts.map +0 -1
- package/src/evaluator/index.ts +0 -14
- package/src/rng/index.ts +0 -8
package/src/index.ts
CHANGED
|
@@ -4,31 +4,45 @@
|
|
|
4
4
|
* @module roll-parser
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
export { RollParserError, isRollParserError } from './errors.js';
|
|
9
|
-
export type { RollParserErrorCode } from './errors.js';
|
|
7
|
+
import { version } from './version.js';
|
|
10
8
|
|
|
11
|
-
|
|
12
|
-
export {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
export type { ErrorSpan, NotationErrorCode, RollParserErrorCode } from './errors.js';
|
|
10
|
+
export {
|
|
11
|
+
getErrorSpan,
|
|
12
|
+
isNotationError,
|
|
13
|
+
isRollParserError,
|
|
14
|
+
NOTATION_ERROR_CODES,
|
|
15
|
+
ROLL_PARSER_ERROR_CODES,
|
|
16
|
+
RollParserError,
|
|
17
|
+
} from './errors.js';
|
|
18
|
+
export {
|
|
19
|
+
DEFAULT_MAX_DICE,
|
|
20
|
+
DEFAULT_MAX_EXPLODE_ITERATIONS,
|
|
21
|
+
DEFAULT_MAX_REROLL_ITERATIONS,
|
|
22
|
+
EvaluatorError,
|
|
23
|
+
evaluate,
|
|
24
|
+
} from './evaluator/evaluator.js';
|
|
25
|
+
export { LexerError, lex } from './lexer/lexer.js';
|
|
26
|
+
export type { Token } from './lexer/tokens.js';
|
|
27
|
+
export { TokenType } from './lexer/tokens.js';
|
|
16
28
|
export type {
|
|
17
29
|
ASTNode,
|
|
18
30
|
BinaryOpNode,
|
|
31
|
+
CritThreshold,
|
|
32
|
+
CritThresholdNode,
|
|
19
33
|
DiceNode,
|
|
20
|
-
|
|
34
|
+
DieBoundNode,
|
|
21
35
|
ExplodeNode,
|
|
22
36
|
FateDiceNode,
|
|
23
37
|
FunctionCallNode,
|
|
24
38
|
GroupedNode,
|
|
39
|
+
GroupNode,
|
|
40
|
+
KeepDropNode,
|
|
25
41
|
LiteralNode,
|
|
26
|
-
|
|
42
|
+
NodeSpan,
|
|
27
43
|
RerollNode,
|
|
28
|
-
SuccessCountNode,
|
|
29
44
|
SortNode,
|
|
30
|
-
|
|
31
|
-
GroupNode,
|
|
45
|
+
SuccessCountNode,
|
|
32
46
|
UnaryOpNode,
|
|
33
47
|
VariableNode,
|
|
34
48
|
VersusNode,
|
|
@@ -37,13 +51,14 @@ export {
|
|
|
37
51
|
isBinaryOp,
|
|
38
52
|
isCritThreshold,
|
|
39
53
|
isDice,
|
|
54
|
+
isDieBound,
|
|
40
55
|
isExplode,
|
|
41
56
|
isFateDice,
|
|
42
57
|
isFunctionCall,
|
|
43
58
|
isGroup,
|
|
44
59
|
isGrouped,
|
|
60
|
+
isKeepDrop,
|
|
45
61
|
isLiteral,
|
|
46
|
-
isModifier,
|
|
47
62
|
isReroll,
|
|
48
63
|
isSort,
|
|
49
64
|
isSuccessCount,
|
|
@@ -51,40 +66,46 @@ export {
|
|
|
51
66
|
isVariable,
|
|
52
67
|
isVersus,
|
|
53
68
|
} from './parser/ast.js';
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
export type { RNG } from './rng/types.js';
|
|
69
|
+
export { MAX_PARSE_DEPTH, ParseError, parse } from './parser/parser.js';
|
|
70
|
+
export type { RngState } from './rng/seeded.js';
|
|
57
71
|
export { SeededRNG } from './rng/seeded.js';
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
export {
|
|
61
|
-
DEFAULT_MAX_DICE,
|
|
62
|
-
DEFAULT_MAX_EXPLODE_ITERATIONS,
|
|
63
|
-
DEFAULT_MAX_REROLL_ITERATIONS,
|
|
64
|
-
evaluate,
|
|
65
|
-
EvaluatorError,
|
|
66
|
-
} from './evaluator/evaluator.js';
|
|
72
|
+
export type { RNG } from './rng/types.js';
|
|
73
|
+
export type { RollOptions } from './roll.js';
|
|
74
|
+
export { roll } from './roll.js';
|
|
67
75
|
export type {
|
|
68
76
|
CompareOp,
|
|
69
77
|
ComparePoint,
|
|
70
78
|
DieModifier,
|
|
71
79
|
DieResult,
|
|
72
80
|
EvaluateOptions,
|
|
73
|
-
|
|
81
|
+
EvaluationOptions,
|
|
82
|
+
KeepDropSpec,
|
|
74
83
|
ResolvedComparePoint,
|
|
75
84
|
ResolvedCritThreshold,
|
|
76
85
|
RollPart,
|
|
86
|
+
RollPartBase,
|
|
77
87
|
RollPartType,
|
|
78
88
|
RollResult,
|
|
79
89
|
} from './types.js';
|
|
80
90
|
export { DegreeOfSuccess } from './types.js';
|
|
81
91
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
92
|
+
/**
|
|
93
|
+
* Installed roll-parser version — the exact string in `package.json`, so
|
|
94
|
+
* pre-releases keep their suffix (`'3.0.0-beta.0'`). Sourced from the
|
|
95
|
+
* generated `src/version.ts`, kept in sync with the manifest by
|
|
96
|
+
* `bun run generate:version` and gated by `check:version` and CI.
|
|
97
|
+
*
|
|
98
|
+
* Useful in bug reports and for feature-gating against a minimum version. The
|
|
99
|
+
* CLI prints it for `--version` and in the `--help` header.
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* ```typescript
|
|
103
|
+
* import { VERSION } from 'roll-parser';
|
|
104
|
+
*
|
|
105
|
+
* VERSION; // e.g. '3.0.0', or '3.1.0-beta.0' on a pre-release
|
|
106
|
+
* VERSION.startsWith('3.'); // true
|
|
107
|
+
* ```
|
|
108
|
+
*
|
|
109
|
+
* @category Core
|
|
110
|
+
*/
|
|
90
111
|
export const VERSION: string = version;
|
package/src/lexer/lexer.ts
CHANGED
|
@@ -5,24 +5,72 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { RollParserErrorCode } from '../errors.js';
|
|
8
|
-
import { RollParserError } from '../errors.js';
|
|
8
|
+
import { describeValue, RollParserError } from '../errors.js';
|
|
9
9
|
import { type Token, TokenType } from './tokens.js';
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Error thrown when the lexer encounters an invalid character.
|
|
13
|
+
*
|
|
14
|
+
* `position` is a zero-based UTF-16 offset into the input. It is deliberately
|
|
15
|
+
* absent from `message` — read it from the field, or uniformly across all
|
|
16
|
+
* roll-parser errors via `getErrorSpan`.
|
|
17
|
+
*
|
|
18
|
+
* Codes: `UNEXPECTED_CHARACTER` for a character that cannot start any token,
|
|
19
|
+
* `UNEXPECTED_IDENTIFIER` for a word that is not a known keyword.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```typescript
|
|
23
|
+
* import { LexerError, roll } from 'roll-parser';
|
|
24
|
+
*
|
|
25
|
+
* try {
|
|
26
|
+
* roll('2d6+&');
|
|
27
|
+
* } catch (error) {
|
|
28
|
+
* const typed = error as LexerError;
|
|
29
|
+
* typed.code; // 'UNEXPECTED_CHARACTER'
|
|
30
|
+
* typed.character; // '&'
|
|
31
|
+
* typed.position; // 4
|
|
32
|
+
* }
|
|
33
|
+
* ```
|
|
34
|
+
*
|
|
35
|
+
* @category Errors
|
|
13
36
|
*/
|
|
14
37
|
export class LexerError extends RollParserError {
|
|
38
|
+
/** Zero-based UTF-16 offset of the offending character in the input. */
|
|
15
39
|
readonly position: number;
|
|
40
|
+
/**
|
|
41
|
+
* The offending text — a single character for `UNEXPECTED_CHARACTER` (the
|
|
42
|
+
* whole code point, so astral symbols are not split into surrogates), or
|
|
43
|
+
* the unrecognized word for `UNEXPECTED_IDENTIFIER`.
|
|
44
|
+
*/
|
|
16
45
|
readonly character: string;
|
|
17
46
|
|
|
18
|
-
constructor(
|
|
19
|
-
|
|
47
|
+
constructor(
|
|
48
|
+
message: string,
|
|
49
|
+
code: RollParserErrorCode,
|
|
50
|
+
position: number,
|
|
51
|
+
character: string,
|
|
52
|
+
options?: ErrorOptions,
|
|
53
|
+
) {
|
|
54
|
+
super(`${message}: '${character}'`, code, options);
|
|
20
55
|
this.name = 'LexerError';
|
|
21
56
|
this.position = position;
|
|
22
57
|
this.character = character;
|
|
23
58
|
}
|
|
24
59
|
}
|
|
25
60
|
|
|
61
|
+
//
|
|
62
|
+
// * Character codes
|
|
63
|
+
//
|
|
64
|
+
|
|
65
|
+
// Range tests compare code units: `char.toLowerCase()` per character allocated
|
|
66
|
+
// a string on the lexer's hottest loop.
|
|
67
|
+
const CHAR_DIGIT_0 = 48;
|
|
68
|
+
const CHAR_DIGIT_9 = 57;
|
|
69
|
+
const CHAR_UPPER_A = 65;
|
|
70
|
+
const CHAR_UPPER_Z = 90;
|
|
71
|
+
const CHAR_LOWER_A = 97;
|
|
72
|
+
const CHAR_LOWER_Z = 122;
|
|
73
|
+
|
|
26
74
|
/** Known identifier keywords mapped to their token types. */
|
|
27
75
|
const IDENTIFIER_KEYWORDS: Record<string, TokenType> = {
|
|
28
76
|
kh: TokenType.KEEP_HIGH,
|
|
@@ -39,6 +87,8 @@ const IDENTIFIER_KEYWORDS: Record<string, TokenType> = {
|
|
|
39
87
|
ceil: TokenType.FUNCTION,
|
|
40
88
|
round: TokenType.FUNCTION,
|
|
41
89
|
abs: TokenType.FUNCTION,
|
|
90
|
+
sqrt: TokenType.FUNCTION,
|
|
91
|
+
pow: TokenType.FUNCTION,
|
|
42
92
|
max: TokenType.FUNCTION,
|
|
43
93
|
min: TokenType.FUNCTION,
|
|
44
94
|
s: TokenType.SORT_ASC,
|
|
@@ -106,22 +156,18 @@ export class Lexer {
|
|
|
106
156
|
const startPos = this.pos;
|
|
107
157
|
const char = this.peek();
|
|
108
158
|
|
|
109
|
-
// * Numbers
|
|
110
159
|
if (this.isDigit(char)) {
|
|
111
160
|
return this.scanNumber();
|
|
112
161
|
}
|
|
113
162
|
|
|
114
|
-
// * Identifiers (d, kh, kl, dh, dl, r, ro, f, vs, floor, ceil, ...)
|
|
115
163
|
if (this.isAlpha(char)) {
|
|
116
164
|
return this.scanIdentifier();
|
|
117
165
|
}
|
|
118
166
|
|
|
119
|
-
// * Variable reference
|
|
120
167
|
if (char === '@') {
|
|
121
168
|
return this.scanAt();
|
|
122
169
|
}
|
|
123
170
|
|
|
124
|
-
// * Operators and punctuation
|
|
125
171
|
this.advance();
|
|
126
172
|
|
|
127
173
|
switch (char) {
|
|
@@ -166,8 +212,7 @@ export class Lexer {
|
|
|
166
212
|
if (this.match('!')) {
|
|
167
213
|
return this.createTokenAt(TokenType.EXPLODE_COMPOUND, '!!', startPos);
|
|
168
214
|
}
|
|
169
|
-
if (
|
|
170
|
-
this.advance();
|
|
215
|
+
if (this.match('p') || this.match('P')) {
|
|
171
216
|
return this.createTokenAt(TokenType.EXPLODE_PENETRATING, '!p', startPos);
|
|
172
217
|
}
|
|
173
218
|
return this.createTokenAt(TokenType.EXPLODE, char, startPos);
|
|
@@ -181,7 +226,9 @@ export class Lexer {
|
|
|
181
226
|
}
|
|
182
227
|
}
|
|
183
228
|
|
|
229
|
+
//
|
|
184
230
|
// * Private helpers
|
|
231
|
+
//
|
|
185
232
|
|
|
186
233
|
private skipWhitespace(): void {
|
|
187
234
|
while (!this.isAtEnd() && this.isWhitespace(this.peek())) {
|
|
@@ -189,24 +236,23 @@ export class Lexer {
|
|
|
189
236
|
}
|
|
190
237
|
}
|
|
191
238
|
|
|
239
|
+
// Scanners slice once from a recorded start offset rather than accumulating
|
|
240
|
+
// `value += this.advance()` — one string per token instead of one per character.
|
|
192
241
|
private scanNumber(): Token {
|
|
193
242
|
const startPos = this.pos;
|
|
194
|
-
let value = '';
|
|
195
243
|
|
|
196
|
-
// Integer part
|
|
197
244
|
while (!this.isAtEnd() && this.isDigit(this.peek())) {
|
|
198
|
-
|
|
245
|
+
this.pos++;
|
|
199
246
|
}
|
|
200
247
|
|
|
201
|
-
// Decimal part
|
|
202
248
|
if (!this.isAtEnd() && this.peek() === '.' && this.isDigit(this.peekNext())) {
|
|
203
|
-
|
|
249
|
+
this.pos++;
|
|
204
250
|
while (!this.isAtEnd() && this.isDigit(this.peek())) {
|
|
205
|
-
|
|
251
|
+
this.pos++;
|
|
206
252
|
}
|
|
207
253
|
}
|
|
208
254
|
|
|
209
|
-
return this.createTokenAt(TokenType.NUMBER,
|
|
255
|
+
return this.createTokenAt(TokenType.NUMBER, this.input.slice(startPos, this.pos), startPos);
|
|
210
256
|
}
|
|
211
257
|
|
|
212
258
|
/**
|
|
@@ -227,18 +273,15 @@ export class Lexer {
|
|
|
227
273
|
const first = this.peek();
|
|
228
274
|
const second = this.peekNext();
|
|
229
275
|
if ((first === 'd' || first === 'D') && (second === 'f' || second === 'F')) {
|
|
230
|
-
this.
|
|
231
|
-
this.advance();
|
|
276
|
+
this.pos += 2;
|
|
232
277
|
return this.createTokenAt(TokenType.DICE_FATE, 'df', startPos);
|
|
233
278
|
}
|
|
234
279
|
|
|
235
|
-
let value = '';
|
|
236
|
-
|
|
237
280
|
while (!this.isAtEnd() && this.isAlpha(this.peek())) {
|
|
238
|
-
|
|
281
|
+
this.pos++;
|
|
239
282
|
}
|
|
240
283
|
|
|
241
|
-
const lower =
|
|
284
|
+
const lower = this.input.slice(startPos, this.pos).toLowerCase();
|
|
242
285
|
|
|
243
286
|
if (lower === 'd' && !this.isAtEnd() && this.peek() === '%') {
|
|
244
287
|
this.advance();
|
|
@@ -272,11 +315,11 @@ export class Lexer {
|
|
|
272
315
|
*/
|
|
273
316
|
private scanAt(): Token {
|
|
274
317
|
const startPos = this.pos;
|
|
275
|
-
this.advance();
|
|
318
|
+
this.advance();
|
|
276
319
|
|
|
277
320
|
let name: string;
|
|
278
321
|
if (!this.isAtEnd() && this.peek() === '{') {
|
|
279
|
-
this.advance();
|
|
322
|
+
this.advance();
|
|
280
323
|
const nameStart = this.pos;
|
|
281
324
|
while (!this.isAtEnd() && this.peek() !== '}' && this.peek() !== '\n') {
|
|
282
325
|
this.advance();
|
|
@@ -285,7 +328,7 @@ export class Lexer {
|
|
|
285
328
|
throw new LexerError('Unterminated @{...} variable', 'UNEXPECTED_CHARACTER', startPos, '@');
|
|
286
329
|
}
|
|
287
330
|
name = this.input.slice(nameStart, this.pos);
|
|
288
|
-
this.advance();
|
|
331
|
+
this.advance();
|
|
289
332
|
} else {
|
|
290
333
|
const nameStart = this.pos;
|
|
291
334
|
if (this.isAtEnd() || !this.isIdentifierStart(this.peek())) {
|
|
@@ -324,13 +367,19 @@ export class Lexer {
|
|
|
324
367
|
return this.pos >= this.input.length;
|
|
325
368
|
}
|
|
326
369
|
|
|
370
|
+
// `NaN` from an empty `peek()` fails every comparison, so end-of-input
|
|
371
|
+
// still reads as "not a digit / not alpha" without an extra guard.
|
|
327
372
|
private isDigit(char: string): boolean {
|
|
328
|
-
|
|
373
|
+
const code = char.charCodeAt(0);
|
|
374
|
+
return code >= CHAR_DIGIT_0 && code <= CHAR_DIGIT_9;
|
|
329
375
|
}
|
|
330
376
|
|
|
331
377
|
private isAlpha(char: string): boolean {
|
|
332
|
-
const
|
|
333
|
-
return
|
|
378
|
+
const code = char.charCodeAt(0);
|
|
379
|
+
return (
|
|
380
|
+
(code >= CHAR_LOWER_A && code <= CHAR_LOWER_Z) ||
|
|
381
|
+
(code >= CHAR_UPPER_A && code <= CHAR_UPPER_Z)
|
|
382
|
+
);
|
|
334
383
|
}
|
|
335
384
|
|
|
336
385
|
private isIdentifierStart(char: string): boolean {
|
|
@@ -345,8 +394,8 @@ export class Lexer {
|
|
|
345
394
|
return char === ' ' || char === '\t' || char === '\n' || char === '\r';
|
|
346
395
|
}
|
|
347
396
|
|
|
348
|
-
//
|
|
349
|
-
//
|
|
397
|
+
// Both factories run after the token's characters were consumed, so
|
|
398
|
+
// `this.pos` is the exclusive end offset.
|
|
350
399
|
private createToken(type: TokenType, value: string): Token {
|
|
351
400
|
return { type, value, position: this.pos, end: this.pos };
|
|
352
401
|
}
|
|
@@ -357,18 +406,42 @@ export class Lexer {
|
|
|
357
406
|
}
|
|
358
407
|
|
|
359
408
|
/**
|
|
360
|
-
*
|
|
409
|
+
* Tokenizes a dice notation string. The first stage of the pipeline —
|
|
410
|
+
* {@link parse} calls it for you; reach for `lex` directly only to build a
|
|
411
|
+
* syntax highlighter or an editor integration.
|
|
412
|
+
*
|
|
413
|
+
* Notation is case-insensitive and whitespace-tolerant: `2D20 + 5` and
|
|
414
|
+
* `2d20+5` produce the same tokens, and identifier tokens carry a lowercased
|
|
415
|
+
* `value`. The one exception is `@name`, whose case is preserved.
|
|
361
416
|
*
|
|
362
417
|
* @param input - The dice notation to tokenize
|
|
363
|
-
* @returns
|
|
364
|
-
*
|
|
418
|
+
* @returns Every token in source order, always ending with one
|
|
419
|
+
* `TokenType.EOF` token
|
|
420
|
+
* @throws {LexerError} If an invalid character or unknown identifier is found
|
|
421
|
+
* @throws {RollParserError} `INVALID_NOTATION_TYPE` when `input` is not a
|
|
422
|
+
* string — raised before scanning, so it carries no position
|
|
365
423
|
*
|
|
366
424
|
* @example
|
|
367
425
|
* ```typescript
|
|
426
|
+
* import { lex, TokenType } from 'roll-parser';
|
|
427
|
+
*
|
|
368
428
|
* const tokens = lex('2d20+5');
|
|
369
|
-
* //
|
|
429
|
+
* tokens.length; // 6 — NUMBER DICE NUMBER PLUS NUMBER EOF
|
|
430
|
+
* tokens[0]; // { type: TokenType.NUMBER, value: '2', position: 0, end: 1 }
|
|
431
|
+
* tokens[1].type === TokenType.DICE; // true
|
|
432
|
+
* tokens.at(-1)?.type === TokenType.EOF; // true
|
|
370
433
|
* ```
|
|
434
|
+
*
|
|
435
|
+
* @category Core
|
|
371
436
|
*/
|
|
372
437
|
export function lex(input: string): Token[] {
|
|
438
|
+
// ! The pipeline's only notation type guard — `parse` and `roll` both funnel through here.
|
|
439
|
+
if (typeof input !== 'string') {
|
|
440
|
+
throw new RollParserError(
|
|
441
|
+
`Notation must be a string, received ${describeValue(input)}`,
|
|
442
|
+
'INVALID_NOTATION_TYPE',
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
|
|
373
446
|
return new Lexer(input).tokenize();
|
|
374
447
|
}
|
package/src/lexer/tokens.ts
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Grouped semantically. Numeric values are stable identifiers — the specific
|
|
11
11
|
* numbers don't matter, but they must be unique.
|
|
12
|
+
*
|
|
13
|
+
* @category AST
|
|
12
14
|
*/
|
|
13
15
|
export enum TokenType {
|
|
14
16
|
//
|
|
@@ -116,7 +118,11 @@ export enum TokenType {
|
|
|
116
118
|
// * Functions
|
|
117
119
|
//
|
|
118
120
|
|
|
119
|
-
/**
|
|
121
|
+
/**
|
|
122
|
+
* Math function: 'floor', 'ceil', 'round', 'abs', 'sqrt', 'pow', 'max',
|
|
123
|
+
* 'min'. In postfix position, 'min'/'max' double as per-die clamp
|
|
124
|
+
* modifiers (`4d6min2`) — the parser decides by position, not the lexer.
|
|
125
|
+
*/
|
|
120
126
|
FUNCTION = 28,
|
|
121
127
|
|
|
122
128
|
//
|
|
@@ -169,19 +175,38 @@ export enum TokenType {
|
|
|
169
175
|
}
|
|
170
176
|
|
|
171
177
|
/**
|
|
172
|
-
* A token produced by the
|
|
178
|
+
* A token produced by {@link lex}: what it is, the text it came from, and
|
|
179
|
+
* where in the input that text sits.
|
|
180
|
+
*
|
|
181
|
+
* `position`/`end` are a half-open range, so `input.slice(position, end)`
|
|
182
|
+
* recovers the original source text — useful for syntax highlighting, where
|
|
183
|
+
* `value` alone is lossy.
|
|
184
|
+
*
|
|
185
|
+
* @example
|
|
186
|
+
* ```typescript
|
|
187
|
+
* import { lex, TokenType } from 'roll-parser';
|
|
188
|
+
*
|
|
189
|
+
* const input = '4D6KH3';
|
|
190
|
+
* const tokens = lex(input);
|
|
191
|
+
* tokens[1]; // { type: TokenType.DICE, value: 'd', position: 1, end: 2 }
|
|
192
|
+
* tokens[3].type === TokenType.KEEP_HIGH; // true
|
|
193
|
+
* input.slice(tokens[3].position, tokens[3].end); // 'KH' — original casing
|
|
194
|
+
* tokens[3].value; // 'kh' — normalized
|
|
195
|
+
* ```
|
|
196
|
+
*
|
|
197
|
+
* @category AST
|
|
173
198
|
*/
|
|
174
199
|
export type Token = {
|
|
175
200
|
/** The type of this token */
|
|
176
|
-
type: TokenType;
|
|
201
|
+
readonly type: TokenType;
|
|
177
202
|
/** The raw string value from input (lowercased for identifiers) */
|
|
178
|
-
value: string;
|
|
203
|
+
readonly value: string;
|
|
179
204
|
/** Zero-based start offset in the input string (UTF-16 code units) */
|
|
180
|
-
position: number;
|
|
205
|
+
readonly position: number;
|
|
181
206
|
/**
|
|
182
207
|
* Zero-based end offset (exclusive). Not always `position + value.length` —
|
|
183
208
|
* braced variables (`@{name}` stores only `name`) and case-normalized
|
|
184
209
|
* identifiers consume more input than their `value` retains.
|
|
185
210
|
*/
|
|
186
|
-
end: number;
|
|
211
|
+
readonly end: number;
|
|
187
212
|
};
|