solve-engine 1.0.0-beta.0 → 1.0.0-beta.2
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/dist/BytecodeBuilder-3egMyuDi.d.cts +274 -0
- package/dist/BytecodeBuilder-3egMyuDi.d.ts +274 -0
- package/dist/Configuration-fVpd-HC-.d.cts +228 -0
- package/dist/Configuration-fVpd-HC-.d.ts +228 -0
- package/dist/EngineError-DvPSnhZi.d.cts +304 -0
- package/dist/EngineError-DvPSnhZi.d.ts +304 -0
- package/dist/Lexer-BJdhlnej.d.cts +531 -0
- package/dist/Lexer-D9l4Yrk2.d.ts +531 -0
- package/dist/NormalizerRule-BrVoVjmP.d.cts +163 -0
- package/dist/NormalizerRule-CEjf1FyD.d.ts +163 -0
- package/dist/PackageRegistry-D-Tv_7ca.d.cts +2572 -0
- package/dist/PackageRegistry-DaH4aIKP.d.ts +2572 -0
- package/dist/Parselet-CK8cQu2a.d.cts +299 -0
- package/dist/Parselet-ConOIdRO.d.ts +299 -0
- package/dist/ScopeManager-B5k0nk05.d.ts +559 -0
- package/dist/ScopeManager-CKfQtl7l.d.cts +559 -0
- package/dist/Token-0jpvUdPY.d.cts +245 -0
- package/dist/Token-0jpvUdPY.d.ts +245 -0
- package/dist/VMCheckpoints-BmDwIXgK.d.ts +171 -0
- package/dist/VMCheckpoints-CqFuLwrT.d.cts +171 -0
- package/dist/Value-KtAQNHur.d.cts +295 -0
- package/dist/Value-KtAQNHur.d.ts +295 -0
- package/dist/chunk-2RWAXT6O.js +3 -0
- package/dist/chunk-2RWAXT6O.js.map +1 -0
- package/dist/chunk-34RRD7PC.js +1492 -0
- package/dist/chunk-34RRD7PC.js.map +1 -0
- package/dist/chunk-3PPFLFH4.js +5097 -0
- package/dist/chunk-3PPFLFH4.js.map +1 -0
- package/dist/chunk-3VGDNEE3.js +84 -0
- package/dist/chunk-3VGDNEE3.js.map +1 -0
- package/dist/chunk-3YNVWKR2.cjs +370 -0
- package/dist/chunk-3YNVWKR2.cjs.map +1 -0
- package/dist/chunk-47LRVGOT.cjs +316 -0
- package/dist/chunk-47LRVGOT.cjs.map +1 -0
- package/dist/chunk-4MG4XKO2.js +1644 -0
- package/dist/chunk-4MG4XKO2.js.map +1 -0
- package/dist/chunk-4QADQTWS.js +2392 -0
- package/dist/chunk-4QADQTWS.js.map +1 -0
- package/dist/chunk-53B6KDDJ.cjs +1651 -0
- package/dist/chunk-53B6KDDJ.cjs.map +1 -0
- package/dist/chunk-5X2PTP6F.cjs +104 -0
- package/dist/chunk-5X2PTP6F.cjs.map +1 -0
- package/dist/chunk-5YEMOYSE.js +264 -0
- package/dist/chunk-5YEMOYSE.js.map +1 -0
- package/dist/chunk-64W6GLLZ.js +481 -0
- package/dist/chunk-64W6GLLZ.js.map +1 -0
- package/dist/chunk-6BKTCEUP.cjs +233 -0
- package/dist/chunk-6BKTCEUP.cjs.map +1 -0
- package/dist/chunk-6NTVRDQV.cjs +1642 -0
- package/dist/chunk-6NTVRDQV.cjs.map +1 -0
- package/dist/chunk-6RDSQURE.js +290 -0
- package/dist/chunk-6RDSQURE.js.map +1 -0
- package/dist/chunk-75JP4BZX.js +93 -0
- package/dist/chunk-75JP4BZX.js.map +1 -0
- package/dist/chunk-B7LXJ6LC.cjs +36 -0
- package/dist/chunk-B7LXJ6LC.cjs.map +1 -0
- package/dist/chunk-BVAR5J2F.js +257 -0
- package/dist/chunk-BVAR5J2F.js.map +1 -0
- package/dist/chunk-C4XZV6E7.cjs +269 -0
- package/dist/chunk-C4XZV6E7.cjs.map +1 -0
- package/dist/chunk-CCJPSHGI.cjs +40 -0
- package/dist/chunk-CCJPSHGI.cjs.map +1 -0
- package/dist/chunk-CLL7RUQV.cjs +316 -0
- package/dist/chunk-CLL7RUQV.cjs.map +1 -0
- package/dist/chunk-DM3LMRBC.js +329 -0
- package/dist/chunk-DM3LMRBC.js.map +1 -0
- package/dist/chunk-DUEZFTK2.js +38 -0
- package/dist/chunk-DUEZFTK2.js.map +1 -0
- package/dist/chunk-EBSPLUW4.cjs +489 -0
- package/dist/chunk-EBSPLUW4.cjs.map +1 -0
- package/dist/chunk-EHAHVROS.cjs +261 -0
- package/dist/chunk-EHAHVROS.cjs.map +1 -0
- package/dist/chunk-EIGTWK5N.js +167 -0
- package/dist/chunk-EIGTWK5N.js.map +1 -0
- package/dist/chunk-EPOXXJBK.js +96 -0
- package/dist/chunk-EPOXXJBK.js.map +1 -0
- package/dist/chunk-FEAIOR7N.cjs +91 -0
- package/dist/chunk-FEAIOR7N.cjs.map +1 -0
- package/dist/chunk-G535KJEG.js +312 -0
- package/dist/chunk-G535KJEG.js.map +1 -0
- package/dist/chunk-GCM5JHMX.js +88 -0
- package/dist/chunk-GCM5JHMX.js.map +1 -0
- package/dist/chunk-GOLDJNMZ.js +368 -0
- package/dist/chunk-GOLDJNMZ.js.map +1 -0
- package/dist/chunk-GW32KPCU.cjs +360 -0
- package/dist/chunk-GW32KPCU.cjs.map +1 -0
- package/dist/chunk-HTXVVJRA.cjs +1549 -0
- package/dist/chunk-HTXVVJRA.cjs.map +1 -0
- package/dist/chunk-HWSZJQCI.js +311 -0
- package/dist/chunk-HWSZJQCI.js.map +1 -0
- package/dist/chunk-IF532O7C.js +101 -0
- package/dist/chunk-IF532O7C.js.map +1 -0
- package/dist/chunk-J73SJHR3.cjs +2415 -0
- package/dist/chunk-J73SJHR3.cjs.map +1 -0
- package/dist/chunk-JBSYC7BB.cjs +7473 -0
- package/dist/chunk-JBSYC7BB.cjs.map +1 -0
- package/dist/chunk-KVILKGMS.js +228 -0
- package/dist/chunk-KVILKGMS.js.map +1 -0
- package/dist/chunk-LIPPNDBE.js +258 -0
- package/dist/chunk-LIPPNDBE.js.map +1 -0
- package/dist/chunk-LR7YASZF.cjs +172 -0
- package/dist/chunk-LR7YASZF.cjs.map +1 -0
- package/dist/chunk-M4N5GA5Z.cjs +309 -0
- package/dist/chunk-M4N5GA5Z.cjs.map +1 -0
- package/dist/chunk-M5LX5AOO.js +7459 -0
- package/dist/chunk-M5LX5AOO.js.map +1 -0
- package/dist/chunk-MZOVP3WI.cjs +96 -0
- package/dist/chunk-MZOVP3WI.cjs.map +1 -0
- package/dist/chunk-N5YFR4UP.cjs +10 -0
- package/dist/chunk-N5YFR4UP.cjs.map +1 -0
- package/dist/chunk-NGBBNL4K.cjs +4 -0
- package/dist/chunk-NGBBNL4K.cjs.map +1 -0
- package/dist/chunk-NH2O2AUR.js +146 -0
- package/dist/chunk-NH2O2AUR.js.map +1 -0
- package/dist/chunk-NLNDMWZC.js +31 -0
- package/dist/chunk-NLNDMWZC.js.map +1 -0
- package/dist/chunk-NMCRQP3Z.cjs +778 -0
- package/dist/chunk-NMCRQP3Z.cjs.map +1 -0
- package/dist/chunk-NMD5VRN4.cjs +1327 -0
- package/dist/chunk-NMD5VRN4.cjs.map +1 -0
- package/dist/chunk-OC6LTXYS.cjs +286 -0
- package/dist/chunk-OC6LTXYS.cjs.map +1 -0
- package/dist/chunk-OT6OJY7C.cjs +114 -0
- package/dist/chunk-OT6OJY7C.cjs.map +1 -0
- package/dist/chunk-PFSDGGYL.js +8 -0
- package/dist/chunk-PFSDGGYL.js.map +1 -0
- package/dist/chunk-QNJ4ACRT.cjs +155 -0
- package/dist/chunk-QNJ4ACRT.cjs.map +1 -0
- package/dist/chunk-QWY3VEZN.js +1637 -0
- package/dist/chunk-QWY3VEZN.js.map +1 -0
- package/dist/chunk-RFYD5TJE.js +111 -0
- package/dist/chunk-RFYD5TJE.js.map +1 -0
- package/dist/chunk-RIN643A3.js +354 -0
- package/dist/chunk-RIN643A3.js.map +1 -0
- package/dist/chunk-RZCWSXTA.cjs +5122 -0
- package/dist/chunk-RZCWSXTA.cjs.map +1 -0
- package/dist/chunk-SDGRK7EP.js +774 -0
- package/dist/chunk-SDGRK7EP.js.map +1 -0
- package/dist/chunk-VEKIWQ3F.cjs +39 -0
- package/dist/chunk-VEKIWQ3F.cjs.map +1 -0
- package/dist/chunk-WQTTOGXC.cjs +99 -0
- package/dist/chunk-WQTTOGXC.cjs.map +1 -0
- package/dist/chunk-X6K3HUOY.cjs +86 -0
- package/dist/chunk-X6K3HUOY.cjs.map +1 -0
- package/dist/chunk-XJCMXB2E.cjs +333 -0
- package/dist/chunk-XJCMXB2E.cjs.map +1 -0
- package/dist/chunk-XVWCOTR6.js +1324 -0
- package/dist/chunk-XVWCOTR6.js.map +1 -0
- package/dist/chunk-YLJ4XMA6.js +33 -0
- package/dist/chunk-YLJ4XMA6.js.map +1 -0
- package/dist/chunk-Z2XP767T.cjs +35 -0
- package/dist/chunk-Z2XP767T.cjs.map +1 -0
- package/dist/chunk-Z4X2RWOF.js +37 -0
- package/dist/chunk-Z4X2RWOF.js.map +1 -0
- package/dist/constants.cjs +22 -0
- package/dist/constants.cjs.map +1 -0
- package/dist/constants.d.cts +12 -0
- package/dist/constants.d.ts +12 -0
- package/dist/constants.js +5 -0
- package/dist/constants.js.map +1 -0
- package/dist/engine.cjs +79 -0
- package/dist/engine.cjs.map +1 -0
- package/dist/engine.d.cts +587 -0
- package/dist/engine.d.ts +587 -0
- package/dist/engine.js +34 -0
- package/dist/engine.js.map +1 -0
- package/dist/errors.cjs +81 -0
- package/dist/errors.cjs.map +1 -0
- package/dist/errors.d.cts +95 -0
- package/dist/errors.d.ts +95 -0
- package/dist/errors.js +4 -0
- package/dist/errors.js.map +1 -0
- package/dist/format.cjs +171 -0
- package/dist/format.cjs.map +1 -0
- package/dist/format.d.cts +51 -0
- package/dist/format.d.ts +51 -0
- package/dist/format.js +168 -0
- package/dist/format.js.map +1 -0
- package/dist/index.cjs +97 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +152 -0
- package/dist/index.d.ts +152 -0
- package/dist/index.js +75 -0
- package/dist/index.js.map +1 -0
- package/dist/language.cjs +308 -0
- package/dist/language.cjs.map +1 -0
- package/dist/language.d.cts +110 -0
- package/dist/language.d.ts +110 -0
- package/dist/language.js +287 -0
- package/dist/language.js.map +1 -0
- package/dist/lexer.cjs +64 -0
- package/dist/lexer.cjs.map +1 -0
- package/dist/lexer.d.cts +56 -0
- package/dist/lexer.d.ts +56 -0
- package/dist/lexer.js +11 -0
- package/dist/lexer.js.map +1 -0
- package/dist/normalizer.cjs +40 -0
- package/dist/normalizer.cjs.map +1 -0
- package/dist/normalizer.d.cts +364 -0
- package/dist/normalizer.d.ts +364 -0
- package/dist/normalizer.js +11 -0
- package/dist/normalizer.js.map +1 -0
- package/dist/packages.cjs +121 -0
- package/dist/packages.cjs.map +1 -0
- package/dist/packages.d.cts +691 -0
- package/dist/packages.d.ts +691 -0
- package/dist/packages.js +24 -0
- package/dist/packages.js.map +1 -0
- package/dist/parser.cjs +55 -0
- package/dist/parser.cjs.map +1 -0
- package/dist/parser.d.cts +46 -0
- package/dist/parser.d.ts +46 -0
- package/dist/parser.js +10 -0
- package/dist/parser.js.map +1 -0
- package/dist/pipeline-CrsRPmsv.d.ts +285 -0
- package/dist/pipeline-DPUQt7yL.d.cts +285 -0
- package/dist/resolvers.cjs +13 -0
- package/dist/resolvers.cjs.map +1 -0
- package/dist/resolvers.d.cts +111 -0
- package/dist/resolvers.d.ts +111 -0
- package/dist/resolvers.js +4 -0
- package/dist/resolvers.js.map +1 -0
- package/dist/services.cjs +21 -0
- package/dist/services.cjs.map +1 -0
- package/dist/services.d.cts +30 -0
- package/dist/services.d.ts +30 -0
- package/dist/services.js +4 -0
- package/dist/services.js.map +1 -0
- package/dist/uom.cjs +56 -0
- package/dist/uom.cjs.map +1 -0
- package/dist/uom.d.cts +224 -0
- package/dist/uom.d.ts +224 -0
- package/dist/uom.js +11 -0
- package/dist/uom.js.map +1 -0
- package/dist/utilities.cjs +32 -0
- package/dist/utilities.cjs.map +1 -0
- package/dist/utilities.d.cts +113 -0
- package/dist/utilities.d.ts +113 -0
- package/dist/utilities.js +7 -0
- package/dist/utilities.js.map +1 -0
- package/dist/variables.cjs +17 -0
- package/dist/variables.cjs.map +1 -0
- package/dist/variables.d.cts +87 -0
- package/dist/variables.d.ts +87 -0
- package/dist/variables.js +4 -0
- package/dist/variables.js.map +1 -0
- package/dist/vm.cjs +284 -0
- package/dist/vm.cjs.map +1 -0
- package/dist/vm.d.cts +68 -0
- package/dist/vm.d.ts +68 -0
- package/dist/vm.js +194 -0
- package/dist/vm.js.map +1 -0
- package/package.json +4 -4
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
import { T as Token } from './Token-0jpvUdPY.js';
|
|
2
|
+
|
|
3
|
+
/** Trie node for multi-word phrase matching. */
|
|
4
|
+
interface PhraseNode {
|
|
5
|
+
/** Complete phrase token type (null = intermediate node) */
|
|
6
|
+
type?: string;
|
|
7
|
+
children: Map<string, PhraseNode>;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* The optimized lookup structure built by TokenClassRegistry.build().
|
|
11
|
+
* Consumed by the Lexer for O(1) keyword → token type lookups and
|
|
12
|
+
* O(word-count) phrase matching.
|
|
13
|
+
*/
|
|
14
|
+
interface TokenLookup {
|
|
15
|
+
/** Lowercase keyword → token type. O(1) Map lookup. */
|
|
16
|
+
keywordToType: Map<string, string>;
|
|
17
|
+
/** Phrase trie for multi-word matching. Root node with children maps.
|
|
18
|
+
* Null if no phrases registered. */
|
|
19
|
+
phraseTrie: PhraseNode | null;
|
|
20
|
+
/** Set of lowercase first-words of all registered phrases.
|
|
21
|
+
* Used by the lexer to emit IDENT (not a phrase keyword) for words
|
|
22
|
+
* that start multi-word phrases, deferring to the PhraseMatcher.
|
|
23
|
+
*
|
|
24
|
+
* Example: "to" is in phraseStartWords because "to the power of" is a phrase.
|
|
25
|
+
* When the lexer sees "to", it emits IDENT and lets the phrase matcher
|
|
26
|
+
* combine "to the power of" into a single CARET token.
|
|
27
|
+
*
|
|
28
|
+
* This prevents plugins from accidentally overriding phrase-start words.
|
|
29
|
+
*/
|
|
30
|
+
phraseStartWords: Set<string>;
|
|
31
|
+
/** Case-sensitive unit names for UNIT fallback after keyword lookup fails. */
|
|
32
|
+
unitNames: ReadonlySet<string>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** What a line is, structurally, before anything tries to evaluate it. */
|
|
36
|
+
type MarkdownLineType = 'expression' | 'prose' | 'heading' | 'blockquote' | 'list' | 'code_fence' | 'math_fence' | 'table' | 'table_separator' | 'hr' | 'wikilink' | 'comment' | 'empty';
|
|
37
|
+
/**
|
|
38
|
+
* What a line is and whether it holds anything to evaluate.
|
|
39
|
+
*
|
|
40
|
+
* Produced by a character-level scan that never consults the keyword, unit or
|
|
41
|
+
* operator tables, so any lexer gives the same answer for the same line. See
|
|
42
|
+
* `__tests__/lexer/LineClassificationIsVocabularyIndependent.spec.ts`.
|
|
43
|
+
*/
|
|
44
|
+
interface LineClassification {
|
|
45
|
+
/** The type of this markdown line */
|
|
46
|
+
type: MarkdownLineType;
|
|
47
|
+
/** Whether this line should be skipped (no expression evaluation) */
|
|
48
|
+
skip: boolean;
|
|
49
|
+
/** Whether the line contains inline solve markers (`s`...``) */
|
|
50
|
+
hasInlineSolve: boolean;
|
|
51
|
+
}
|
|
52
|
+
/** Inline solve position with precise coordinates */
|
|
53
|
+
interface InlineSolveSpan {
|
|
54
|
+
/** Character offset of the `s`` marker */
|
|
55
|
+
start: number;
|
|
56
|
+
/** Character offset past the closing `` ` `` */
|
|
57
|
+
end: number;
|
|
58
|
+
/** The expression text between the backticks */
|
|
59
|
+
expression: string;
|
|
60
|
+
/** 1-based column of the `s`` marker */
|
|
61
|
+
columnNumber: number;
|
|
62
|
+
/** Token index of INLINE_SOLVE_START in the line's token array (set during tokenization). */
|
|
63
|
+
startTokenIndex?: number;
|
|
64
|
+
/** Token index of closing BACKTICK_OPEN in the line's token array (set during tokenization). */
|
|
65
|
+
endTokenIndex?: number;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Result from a single line processed by scanDocument().
|
|
69
|
+
* Combines line classification, tokenized tokens, and inline solve spans
|
|
70
|
+
* into a single structure, eliminating the need for separate classifyLine()
|
|
71
|
+
* findInlineSolves(), and per-line Lexer.reset() calls.
|
|
72
|
+
*/
|
|
73
|
+
interface ScanLineResult {
|
|
74
|
+
/** The raw line text (without trailing newline). */
|
|
75
|
+
text: string;
|
|
76
|
+
/** 1-based line number within the document. */
|
|
77
|
+
lineNumber: number;
|
|
78
|
+
/** Character offset of the line start within the document. */
|
|
79
|
+
startOffset: number;
|
|
80
|
+
/** Character offset of the line end (before newline). */
|
|
81
|
+
endOffset: number;
|
|
82
|
+
/** The line classification. */
|
|
83
|
+
classification: LineClassification;
|
|
84
|
+
/** Tokenized tokens (empty array if line is skipped). */
|
|
85
|
+
tokens: Token[];
|
|
86
|
+
/** Inline solve spans found in this line (empty if none). */
|
|
87
|
+
inlineSolves: InlineSolveSpan[];
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* A token, as the lexer produces it.
|
|
91
|
+
*
|
|
92
|
+
* A class rather than an object literal because tokens are created on every
|
|
93
|
+
* keystroke and a shared hidden class keeps that path predictable for the
|
|
94
|
+
* engine running it.
|
|
95
|
+
*/
|
|
96
|
+
declare class LexerToken implements Token {
|
|
97
|
+
type: string;
|
|
98
|
+
typeId: number;
|
|
99
|
+
value: string;
|
|
100
|
+
text: string;
|
|
101
|
+
offset: number;
|
|
102
|
+
lineBreaks: number;
|
|
103
|
+
line: number;
|
|
104
|
+
col: number;
|
|
105
|
+
constructor(type: string, typeId: number, value: string, text: string, offset: number, lineBreaks: number, line: number, col: number);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Plugin interface for extending the ExpressionLexer with custom tokens.
|
|
109
|
+
*
|
|
110
|
+
* Plugins can register:
|
|
111
|
+
* - `keywords`: Map identifier strings to custom token types (checked after locale keywords).
|
|
112
|
+
* - `operators`: Map multi-character operator sequences to custom token types.
|
|
113
|
+
* - `units`: Register additional unit identifiers (checked alongside built-in units).
|
|
114
|
+
*
|
|
115
|
+
* Multi-word phrase matching has been moved to the TokenNormalizer post-lexer
|
|
116
|
+
* stage. To register phrase patterns, use `IEnginePackage.normalizerRules` instead.
|
|
117
|
+
*
|
|
118
|
+
* All registrations are additive, built-in patterns still work.
|
|
119
|
+
*/
|
|
120
|
+
interface LexerVocabulary {
|
|
121
|
+
/**
|
|
122
|
+
* Keyword → tokenType mappings. Each key is a lowercase identifier that,
|
|
123
|
+
* when encountered, will emit the specified token type instead of IDENT.
|
|
124
|
+
* These are checked AFTER the locale's built-in keywordMap, so locale
|
|
125
|
+
* keywords take priority.
|
|
126
|
+
*/
|
|
127
|
+
keywords?: Record<string, string>;
|
|
128
|
+
/**
|
|
129
|
+
* Multi-character operator → tokenType mappings. Each key is the exact
|
|
130
|
+
* character sequence (e.g., "::", "->", "=>") and the value is the token
|
|
131
|
+
* type to emit. Two-character operators take priority during matching.
|
|
132
|
+
* Built-in operators (==, !=, >=, <=, <<, >>) always take priority.
|
|
133
|
+
*/
|
|
134
|
+
operators?: Record<string, string>;
|
|
135
|
+
/**
|
|
136
|
+
* Additional unit identifiers to recognize (e.g., "gp", "osrs", "tile").
|
|
137
|
+
* These are checked alongside the built-in `knownUnits` set.
|
|
138
|
+
*/
|
|
139
|
+
units?: string[];
|
|
140
|
+
/**
|
|
141
|
+
* Whole-line patterns matched against the RAW line text, BEFORE any
|
|
142
|
+
* per-character tokenization begins.
|
|
143
|
+
*
|
|
144
|
+
* Every other extension point in this file (`keywords`/`operators`/
|
|
145
|
+
* `units`, plus `IEnginePackage.phrases`/`normalizerRules`) transforms
|
|
146
|
+
* a token STREAM -- they all assume the line is, at some granularity,
|
|
147
|
+
* valid Solve syntax. This hook exists for the one shape that isn't:
|
|
148
|
+
* a package whose grammar captures arbitrary free-form text terminated
|
|
149
|
+
* by a fixed marker (e.g. a natural-language query ending in `= ?`),
|
|
150
|
+
* where the text itself ("distance to the moon") would never tokenize
|
|
151
|
+
* or parse as a normal expression and must be captured verbatim
|
|
152
|
+
* instead -- see `packages/knowledge/` for the reference use.
|
|
153
|
+
*
|
|
154
|
+
* Each entry's `pattern` is tested (via `RegExp.exec`) against the
|
|
155
|
+
* full, untrimmed line text. If it matches AND capture group 1 is
|
|
156
|
+
* non-empty after trimming, the ENTIRE line becomes a single
|
|
157
|
+
* synthetic token of `tokenType` whose `value`/`text` is the trimmed
|
|
158
|
+
* capture group -- the character-by-character scanner never runs for
|
|
159
|
+
* that line. Patterns are tried in registration order; the first
|
|
160
|
+
* match wins. A package registering a rule here still needs a
|
|
161
|
+
* `prefixParselets` entry for `tokenType` to actually consume the
|
|
162
|
+
* resulting token.
|
|
163
|
+
*
|
|
164
|
+
* Because this bypasses tokenization entirely, a matching line can
|
|
165
|
+
* contain characters that would otherwise be lexer errors (unmatched
|
|
166
|
+
* quotes, stray symbols, ...) -- by design, since the whole point is
|
|
167
|
+
* to hand the package raw text the normal pipeline was never meant to
|
|
168
|
+
* parse.
|
|
169
|
+
*/
|
|
170
|
+
rawLinePatterns?: Array<{
|
|
171
|
+
pattern: RegExp;
|
|
172
|
+
tokenType: string;
|
|
173
|
+
}>;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Character-by-character tokenizer for expression text.
|
|
177
|
+
*
|
|
178
|
+
* Scans a raw line/expression string into a stream of typed tokens
|
|
179
|
+
* (numbers, identifiers, operators, units, keywords, ...), handling
|
|
180
|
+
* markdown-line classification (`classifyLine`), inline `` s`...` `` solve
|
|
181
|
+
* spans, and package-contributed vocabulary (registered via
|
|
182
|
+
* {@link registerVocabulary}/{@link unregisterVocabulary}, keywords
|
|
183
|
+
* operators, and units a package wants recognized as their own token
|
|
184
|
+
* types rather than falling through to generic identifiers).
|
|
185
|
+
*
|
|
186
|
+
* Most consumers should use the higher-level {@link Lexer} wrapper, which
|
|
187
|
+
* adds streaming `next()`/`peek()` access over this class's scan results.
|
|
188
|
+
*/
|
|
189
|
+
declare class ExpressionLexer {
|
|
190
|
+
private static readonly CHAR_CLASS;
|
|
191
|
+
/**
|
|
192
|
+
* Configured TokenLookup from TokenClassRegistry. When set, replaces
|
|
193
|
+
* the internal keyword map and unit set with registry-built equivalents.
|
|
194
|
+
* Enables data-driven keyword/unit registration across locale keywords,
|
|
195
|
+
* provider keywords, and plugins.
|
|
196
|
+
*
|
|
197
|
+
* Set at construction time via the constructor parameter. Plugin-registered
|
|
198
|
+
* keywords/units (via registerVocabulary()) are checked alongside
|
|
199
|
+
* the configuredLookup, neither source is bypassed.
|
|
200
|
+
*/ private configuredLookup;
|
|
201
|
+
private input;
|
|
202
|
+
private pos;
|
|
203
|
+
private len;
|
|
204
|
+
private line;
|
|
205
|
+
private lineStartPos;
|
|
206
|
+
private keywordMap;
|
|
207
|
+
private mergedKeywords;
|
|
208
|
+
private mergedUnits;
|
|
209
|
+
private pluginKeywordMap;
|
|
210
|
+
private pluginOperators;
|
|
211
|
+
private pluginUnits;
|
|
212
|
+
private hasPluginOps;
|
|
213
|
+
private localeCode;
|
|
214
|
+
private locale;
|
|
215
|
+
/**
|
|
216
|
+
* Inline solve spans collected during the most recent tokenization pass.
|
|
217
|
+
* Populated by [Symbol.iterator]() and consumed by scanDocument().
|
|
218
|
+
*/
|
|
219
|
+
_inlineSolveSpans: InlineSolveSpan[];
|
|
220
|
+
private pluginRawLinePatterns;
|
|
221
|
+
/**
|
|
222
|
+
* If a `rawLinePatterns` rule matches the FULL text most recently passed
|
|
223
|
+
* to {@link reset}, this holds the single synthetic token that
|
|
224
|
+
* {@link tokenizeAll} should return instead of running the
|
|
225
|
+
* character-by-character scanner. Cleared (re-evaluated) on every
|
|
226
|
+
* {@link reset} call. `null` when no plugin registered any raw-line
|
|
227
|
+
* patterns, or none matched, the overwhelmingly common case, checked
|
|
228
|
+
* with a `length === 0` guard before ever touching this field so a
|
|
229
|
+
* plugin-free lexer pays zero cost for the feature.
|
|
230
|
+
*/
|
|
231
|
+
private pendingRawLineToken;
|
|
232
|
+
/**
|
|
233
|
+
* Test `text` against every registered `rawLinePatterns` rule, in
|
|
234
|
+
* registration order. Returns a synthetic token for the first rule
|
|
235
|
+
* whose `pattern` matches AND whose capture group 1 is non-empty after
|
|
236
|
+
* trimming; returns `null` if no rule matches (the normal
|
|
237
|
+
* character-by-character scanner should run instead).
|
|
238
|
+
*/
|
|
239
|
+
private matchRawLine;
|
|
240
|
+
/** Rebuild merged keyword and unit collections after plugin registration. */
|
|
241
|
+
private rebuildMergedCollections;
|
|
242
|
+
constructor(localeCode?: string, lookup?: TokenLookup);
|
|
243
|
+
/**
|
|
244
|
+
* Register a plugin to extend the lexer with custom tokens.
|
|
245
|
+
*
|
|
246
|
+
* All registrations are additive, built-in patterns still work.
|
|
247
|
+
* Keywords, operators, and units from the plugin are merged
|
|
248
|
+
* with existing ones. Calling multiple times adds more entries.
|
|
249
|
+
*
|
|
250
|
+
* Note: multi-word phrases are now handled by the TokenNormalizer
|
|
251
|
+
* (see `IEnginePackage.normalizerRules`), not the lexer.
|
|
252
|
+
*
|
|
253
|
+
* Built-in tokens CANNOT be overridden. Throws a EngineError if the
|
|
254
|
+
* plugin attempts to register a keyword, operator, or unit
|
|
255
|
+
* that conflicts with a built-in one.
|
|
256
|
+
*/
|
|
257
|
+
registerVocabulary(plugin: LexerVocabulary): void;
|
|
258
|
+
/**
|
|
259
|
+
* Unregister a plugin, removing its custom tokens from the lexer.
|
|
260
|
+
*
|
|
261
|
+
* This is the inverse of registerVocabulary(). All keywords, operators,
|
|
262
|
+
* and units registered by the plugin are removed. After
|
|
263
|
+
* unregistration, those tokens will revert to their default behavior
|
|
264
|
+
* (e.g., keywords become IDENT, operators become ERROR).
|
|
265
|
+
*
|
|
266
|
+
* Calling unregisterVocabulary with a plugin that was never registered
|
|
267
|
+
* is safe, it simply has no effect.
|
|
268
|
+
*/
|
|
269
|
+
unregisterVocabulary(plugin: LexerVocabulary): void;
|
|
270
|
+
reset(input: string): void;
|
|
271
|
+
/**
|
|
272
|
+
* Scan a full document text in a single pass, classifying each line and
|
|
273
|
+
* tokenizing non-skipped lines.
|
|
274
|
+
*
|
|
275
|
+
* Replaces the separate classifyLine() + findInlineSolves() + per-line
|
|
276
|
+
* reset() + tokenizeAll() pattern with a single character-by-character
|
|
277
|
+
* walk through the entire document. Key benefits:
|
|
278
|
+
*
|
|
279
|
+
* - **Single reset()**: `this.pos`, `this.len`, `this.line`, and
|
|
280
|
+
* `this.lineStartPos` are set once for the whole document, not per-line.
|
|
281
|
+
* - **Single classification**: classifyLine() runs once per line inline;
|
|
282
|
+
* skipped lines are jumped over without tokenization.
|
|
283
|
+
* - **Shared tokenization**: Non-skipped lines are tokenized using the
|
|
284
|
+
* existing state machine, yielding Token[] without per-line reset().
|
|
285
|
+
* - **Inline solve detection**: findInlineSolves() is called only for
|
|
286
|
+
* lines that classifyLine() marks as having inline solves.
|
|
287
|
+
*
|
|
288
|
+
* Tokenization is scoped to each line by temporarily restricting
|
|
289
|
+
* `this.len` to the line end position, so the [Symbol.iterator]
|
|
290
|
+
* generator naturally stops at the line boundary. After tokenization,
|
|
291
|
+
* `this.len` is restored and `this.pos` advances past the newline.
|
|
292
|
+
*
|
|
293
|
+
* @param text The full document text (with newlines).
|
|
294
|
+
* @returns Array of ScanLineResult, one per line, in document order.
|
|
295
|
+
*/
|
|
296
|
+
scanDocument(text: string): ScanLineResult[];
|
|
297
|
+
/**
|
|
298
|
+
* Tokenize an expression string into an array of Tokens.
|
|
299
|
+
*
|
|
300
|
+
* Delegates to the lazy [Symbol.iterator]() generator and collects all
|
|
301
|
+
* yielded tokens via Array.from(). For memory-sensitive use cases, prefer
|
|
302
|
+
* iterating the lexer directly with for...of to avoid array allocation.
|
|
303
|
+
*
|
|
304
|
+
* Optimizations:
|
|
305
|
+
* - CHAR_CLASS jump table (Uint8Array) → switch on small integers
|
|
306
|
+
* - Direct character-code dispatch (c0 cached pattern)
|
|
307
|
+
* - Mathematical digit parsing (integer math, not slice+parseFloat)
|
|
308
|
+
* - Inline operator tokenizer with two-char peek-ahead
|
|
309
|
+
* - Whitespace eliminated in-lexer (never emitted)
|
|
310
|
+
* - 0-char and 1-char fast paths
|
|
311
|
+
*/
|
|
312
|
+
tokenizeAll(): Token[];
|
|
313
|
+
/**
|
|
314
|
+
* Lazy token-by-token generator. Yields each token without allocating an
|
|
315
|
+
* intermediate Token[] array. Supports for...of and spread usage.
|
|
316
|
+
*
|
|
317
|
+
* Usage:
|
|
318
|
+
* for (const t of lexer) { ... } // lazy, no array allocation
|
|
319
|
+
* const tokens = [...lexer]; // materializes via spread
|
|
320
|
+
* const tokens = lexer.tokenizeAll(); // materializes via Array.from()
|
|
321
|
+
*
|
|
322
|
+
* IMPORTANT: This generator captures `this.len` ONCE at creation time
|
|
323
|
+
* (const len = this.len). `scanDocument()` relies on this behavior to
|
|
324
|
+
* scope tokenization to a single line by temporarily restricting
|
|
325
|
+
* `this.len` to the line end position before creating the iterator.
|
|
326
|
+
* Do NOT refactor to re-read `this.len` mid-loop without also updating
|
|
327
|
+
* `scanDocument()`.
|
|
328
|
+
*/
|
|
329
|
+
[Symbol.iterator](): Generator<Token, void, undefined>;
|
|
330
|
+
/**
|
|
331
|
+
* Character-by-character number parsing.
|
|
332
|
+
*
|
|
333
|
+
* Supports: integers, floats, scientific notation (1.5e10, 1.5e-10),
|
|
334
|
+
* hex (0xFF), binary (0b1010), octal (0o17), BigInt suffix (123n),
|
|
335
|
+
* thousands separators (1,234 or 1.234.567).
|
|
336
|
+
*
|
|
337
|
+
* Returns a LexerToken and advances `this.pos` past the number.
|
|
338
|
+
*/
|
|
339
|
+
private tokenizeNumber;
|
|
340
|
+
/**
|
|
341
|
+
* Reads [a-zA-Z_][a-zA-Z0-9_]* and resolves to:
|
|
342
|
+
* - A unit type (via knownUnits, case-sensitive)
|
|
343
|
+
* - A keyword type (via locale keywordMap, case-insensitive)
|
|
344
|
+
* - IDENT if none of the above
|
|
345
|
+
*
|
|
346
|
+
* Multi-word phrases (e.g., "to the power of") are handled by the
|
|
347
|
+
* TokenNormalizer post-lexer pass, not the lexer.
|
|
348
|
+
*/
|
|
349
|
+
private tokenizeIdentifier;
|
|
350
|
+
/**
|
|
351
|
+
* Peek past in-expression whitespace (space, tab) from `pos` to check
|
|
352
|
+
* if the next significant character is '('.
|
|
353
|
+
*/
|
|
354
|
+
private isFollowedByLParen;
|
|
355
|
+
/**
|
|
356
|
+
* Reads an operator/punctuation token.
|
|
357
|
+
* Handles two-char operators (==, !=, >=, <=, **) and the special
|
|
358
|
+
* cases << (LSHIFT) and >> (RSHIFT).
|
|
359
|
+
*/
|
|
360
|
+
private tokenizeOperator;
|
|
361
|
+
/**
|
|
362
|
+
* Reads a double-quoted string literal. Supports backslash escapes.
|
|
363
|
+
*/
|
|
364
|
+
private tokenizeString;
|
|
365
|
+
/**
|
|
366
|
+
* L1 expression gating: quickly determine if a line contains any
|
|
367
|
+
* characters that indicate an expression (digits, operators, currency,
|
|
368
|
+
* backticks, parentheses, etc.).
|
|
369
|
+
*
|
|
370
|
+
* Pure prose lines (e.g., "The quick brown fox jumps over the lazy dog")
|
|
371
|
+
* return false and can be skipped without full tokenization (L2).
|
|
372
|
+
*
|
|
373
|
+
* This is a fast character-by-character scan that stops at the first
|
|
374
|
+
* expression indicator. Called once per line in classifyFromPositions().
|
|
375
|
+
*/
|
|
376
|
+
static hasExpressionIndicators(input: string, start: number, end: number): boolean;
|
|
377
|
+
/**
|
|
378
|
+
* Classify a line by its character positions within this.input.
|
|
379
|
+
* Reads directly from this.input using start/end boundaries.
|
|
380
|
+
* DOES NOT modify this.pos, purely a read-only classifier.
|
|
381
|
+
*/
|
|
382
|
+
private classifyFromPositions;
|
|
383
|
+
/**
|
|
384
|
+
* Classify a single line of markdown text.
|
|
385
|
+
*/
|
|
386
|
+
classifyLine(lineText: string): LineClassification;
|
|
387
|
+
/**
|
|
388
|
+
* Every keyword this lexer currently recognizes, locale keywords
|
|
389
|
+
* (`pi`, `sqrt`, `convert`, ...) merged with any plugin-contributed ones
|
|
390
|
+
* from `registerVocabulary()` (e.g. a package's custom keywords), mapped to
|
|
391
|
+
* the token type they lex to. A snapshot copy, not a live reference
|
|
392
|
+
* mutating the return value has no effect on the lexer.
|
|
393
|
+
*/
|
|
394
|
+
getKeywords(): Record<string, string>;
|
|
395
|
+
/**
|
|
396
|
+
* Find all inline solve markers in a line with precise coordinate mapping.
|
|
397
|
+
*/
|
|
398
|
+
findInlineSolves(lineText: string): InlineSolveSpan[];
|
|
399
|
+
/**
|
|
400
|
+
* Reads a comment: # to end of line, or // to end of line.
|
|
401
|
+
*/
|
|
402
|
+
private tokenizeComment;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Lexer state machine modes.
|
|
407
|
+
* - Main: document-level scanning with markdown classification
|
|
408
|
+
* - Inline: expression embedded in markdown inline solve (`s\`...\``)
|
|
409
|
+
* - String: inside a double-quoted string literal
|
|
410
|
+
*/
|
|
411
|
+
declare enum LexerState {
|
|
412
|
+
Main = "main",
|
|
413
|
+
Inline = "inline",
|
|
414
|
+
String = "string"
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Editor-agnostic semantic classification for a lexer token.
|
|
419
|
+
*
|
|
420
|
+
* This is the entire contract solve-js's language service exposes to any
|
|
421
|
+
* editor integration (CodeMirror, VS Code, ...): a category, nothing about
|
|
422
|
+
* how it should be rendered. CSS class names, VS Code semantic-token
|
|
423
|
+
* indices, etc. are all downstream adapter concerns. See
|
|
424
|
+
* `language/adapters/`.
|
|
425
|
+
*
|
|
426
|
+
* The built-in categories cover every core (non-plugin) token type. Solve-js
|
|
427
|
+
* packages may contribute additional, open-ended category strings for their
|
|
428
|
+
* own custom token types (see `registerTokenCategory` in TokenCategoryMap.ts)
|
|
429
|
+
*, the `(string & {})` union member keeps those assignable without widening
|
|
430
|
+
* the whole type to a bare `string` and losing autocomplete for the built-ins.
|
|
431
|
+
*/
|
|
432
|
+
type TokenCategory = "number" | "string" | "keyword" | "operator" | "comparison" | "bitwise" | "function" | "variable" | "unit" | "datetime" | "vector" | "punctuation" | "error" | (string & {});
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Public tokenizer wrapper around {@link ExpressionLexer}.
|
|
436
|
+
*
|
|
437
|
+
* `ExpressionLexer` does the actual character-by-character scanning;
|
|
438
|
+
* `Lexer` adds a materialized-token-array streaming interface
|
|
439
|
+
* (`next()`/`peek()`) plus line-classification state (`reset()`) so
|
|
440
|
+
* callers can iterate a line's tokens without re-scanning on each peek.
|
|
441
|
+
*
|
|
442
|
+
* Each `ExpressionEngine` instance owns its own `Lexer`, and packages
|
|
443
|
+
* extend it via {@link registerVocabulary} (keywords, operators, units)
|
|
444
|
+
* see `IEnginePackage.lexerVocabulary`.
|
|
445
|
+
*/
|
|
446
|
+
declare class Lexer {
|
|
447
|
+
/** Expression-mode lexer (Phase A: V8-optimized, replaces moo) */
|
|
448
|
+
private expressionLexer;
|
|
449
|
+
private currentState;
|
|
450
|
+
private peekedToken;
|
|
451
|
+
private hasPeeked;
|
|
452
|
+
private tokens;
|
|
453
|
+
private tokenIdx;
|
|
454
|
+
/**
|
|
455
|
+
* @param localeCode - Locale code (e.g., "en", "de"). Defaults to "en".
|
|
456
|
+
* @param tokenLookup - Optional TokenLookup from TokenClassRegistry.
|
|
457
|
+
* When provided, configures ExpressionLexer to use registry-built
|
|
458
|
+
* keyword/unit/phrase lookups instead of internal instance maps.
|
|
459
|
+
*/
|
|
460
|
+
constructor(localeCode?: string, tokenLookup?: TokenLookup);
|
|
461
|
+
reset(input: string, state?: LexerState): void;
|
|
462
|
+
/**
|
|
463
|
+
* Classify a single line of markdown text (Phase B).
|
|
464
|
+
* Delegates to the ExpressionLexer's character-by-character scanner.
|
|
465
|
+
*/
|
|
466
|
+
classifyLine(lineText: string): LineClassification;
|
|
467
|
+
/**
|
|
468
|
+
* Find all inline solve markers in a line (Phase B).
|
|
469
|
+
* Delegates to the ExpressionLexer's character-by-character scanner.
|
|
470
|
+
*/
|
|
471
|
+
findInlineSolves(lineText: string): InlineSolveSpan[];
|
|
472
|
+
/**
|
|
473
|
+
* Every keyword this lexer currently recognizes (locale + plugin-contributed),
|
|
474
|
+
* mapped to the token type it lexes to. Delegates to the ExpressionLexer.
|
|
475
|
+
*/
|
|
476
|
+
getKeywords(): Record<string, string>;
|
|
477
|
+
next(): Token | undefined;
|
|
478
|
+
peek(): Token | undefined;
|
|
479
|
+
[Symbol.iterator](): Iterator<Token>;
|
|
480
|
+
/**
|
|
481
|
+
* Register a plugin to extend the lexer with custom tokens.
|
|
482
|
+
* Delegates to the underlying ExpressionLexer.
|
|
483
|
+
*
|
|
484
|
+
* @see LexerVocabulary for the supported extension points.
|
|
485
|
+
*/
|
|
486
|
+
registerVocabulary(plugin: LexerVocabulary): void;
|
|
487
|
+
/**
|
|
488
|
+
* Unregister a plugin, removing its custom tokens from the lexer.
|
|
489
|
+
* Delegates to the underlying ExpressionLexer.
|
|
490
|
+
*/
|
|
491
|
+
unregisterVocabulary(plugin: LexerVocabulary): void;
|
|
492
|
+
/**
|
|
493
|
+
* Reset the lexer for expression-only text, skips the classifyLine()
|
|
494
|
+
* overhead in reset() for callers that already know the input is an
|
|
495
|
+
* evaluable expression (e.g., after isEmptyLine() confirmed non-skip).
|
|
496
|
+
*/
|
|
497
|
+
resetExpression(input: string): void;
|
|
498
|
+
/**
|
|
499
|
+
* Scan a full document in one pass, classifying each line and
|
|
500
|
+
* tokenizing non-skipped lines. Delegates to ExpressionLexer.
|
|
501
|
+
*
|
|
502
|
+
* @returns ScanLineResult[], one per line, with classification + tokens.
|
|
503
|
+
*/
|
|
504
|
+
scanDocument(text: string): ScanLineResult[];
|
|
505
|
+
getState(): LexerState;
|
|
506
|
+
setState(state: LexerState): void;
|
|
507
|
+
getHighlightTokens(lineText: string): {
|
|
508
|
+
type: string;
|
|
509
|
+
value: string;
|
|
510
|
+
offset: number;
|
|
511
|
+
col: number;
|
|
512
|
+
length: number;
|
|
513
|
+
category: TokenCategory | undefined;
|
|
514
|
+
}[];
|
|
515
|
+
private collectHighlightTokens;
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* A lexer for operations that do not depend on registered vocabulary.
|
|
519
|
+
*
|
|
520
|
+
* Line classification and inline-solve detection read characters looking for
|
|
521
|
+
* headings, comment markers, fences and backtick spans, and never consult the
|
|
522
|
+
* keyword, unit or operator tables. Every lexer therefore returns the same
|
|
523
|
+
* answer, so the callers that have no engine to ask can use this one. Checked
|
|
524
|
+
* by `__tests__/lexer/LineClassificationIsVocabularyIndependent.spec.ts`.
|
|
525
|
+
*
|
|
526
|
+
* Do not tokenize with this. An engine's own lexer carries the vocabulary its
|
|
527
|
+
* packages registered; this one carries none.
|
|
528
|
+
*/
|
|
529
|
+
declare const sharedLexer: Lexer;
|
|
530
|
+
|
|
531
|
+
export { ExpressionLexer as E, type InlineSolveSpan as I, Lexer as L, type MarkdownLineType as M, type ScanLineResult as S, type TokenLookup as T, LexerState as a, LexerToken as b, type LexerVocabulary as c, type LineClassification as d, type TokenCategory as e, sharedLexer as s };
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { T as Token } from './Token-0jpvUdPY.cjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* NormalizerRule, pluggable token normalization rule for the
|
|
5
|
+
* TokenNormalizer post-lexer pass.
|
|
6
|
+
*
|
|
7
|
+
* ## Purpose
|
|
8
|
+
* After the ExpressionLexer produces raw tokens (numbers, identifiers,
|
|
9
|
+
* operators, etc.), the TokenNormalizer applies domain-specific rules
|
|
10
|
+
* to transform the token stream before parsing. This keeps the lexer
|
|
11
|
+
* focused on single-token production and moves multi-token pattern
|
|
12
|
+
* matching into a dedicated normalization layer.
|
|
13
|
+
*
|
|
14
|
+
* ## How rules work
|
|
15
|
+
* Rules are applied in priority order (highest first). At each token
|
|
16
|
+
* position, the normalizer tries every rule in priority order until one
|
|
17
|
+
* matches. Matched tokens are consumed and replaced; unmatched tokens
|
|
18
|
+
* pass through unchanged.
|
|
19
|
+
*
|
|
20
|
+
* ## What rules can do
|
|
21
|
+
* - **Phrase fusion**: Merge consecutive words into compound tokens
|
|
22
|
+
* (e.g., `"to" "the" "power" "of"` → `CARET`)
|
|
23
|
+
* - **Implicit operators**: Insert missing operators between tokens
|
|
24
|
+
* (e.g., `NUMBER IDENT` → `NUMBER STAR IDENT`)
|
|
25
|
+
* - **Domain transformations**: Coalesce item names, currency pairs, etc.
|
|
26
|
+
*
|
|
27
|
+
* ## Why a separate file?
|
|
28
|
+
* This is a duplicate-free copy of the interface defined in
|
|
29
|
+
* TokenNormalizer.ts. Storing it in a separate file avoids circular
|
|
30
|
+
* imports, TokenNormalizer imports NormalizerRule, and rule factories
|
|
31
|
+
* import TokenNormalizer's `createFusedToken`.
|
|
32
|
+
*
|
|
33
|
+
* @module NormalizerRule
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Result of a successful rule match attempt against the token stream.
|
|
38
|
+
*
|
|
39
|
+
* When a {@link NormalizerRule.match} function finds a pattern at the
|
|
40
|
+
* current position, it returns a NormalizerMatch describing how many
|
|
41
|
+
* tokens to consume and what to replace them with.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```ts
|
|
45
|
+
* // The phrase "to the power of" (5 tokens) becomes a single CARET token
|
|
46
|
+
* const match: NormalizerMatch = {
|
|
47
|
+
* consumed: 5,
|
|
48
|
+
* replacement: [caretToken],
|
|
49
|
+
* };
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
interface NormalizerMatch {
|
|
53
|
+
/**
|
|
54
|
+
* Number of tokens consumed from the stream at the match position.
|
|
55
|
+
* Must be ≥ 1, a match always advances the cursor.
|
|
56
|
+
*/
|
|
57
|
+
consumed: number;
|
|
58
|
+
/**
|
|
59
|
+
* Replacement tokens to insert at the match position.
|
|
60
|
+
* May be empty (deletion), a single token (fusion), or multiple
|
|
61
|
+
* tokens (expansion/splitting).
|
|
62
|
+
*/
|
|
63
|
+
replacement: Token[];
|
|
64
|
+
/**
|
|
65
|
+
* Human-readable rule name for diagnostic fusion tracking.
|
|
66
|
+
* When set, the normalizer uses this instead of the rule's `name`
|
|
67
|
+
* in {@link TokenFusion} records. Used by {@link PhraseTrie} to
|
|
68
|
+
* report which specific phrase matched (e.g., "phrase:to the power of").
|
|
69
|
+
*/
|
|
70
|
+
ruleName?: string;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* A pluggable normalization rule registered with the TokenNormalizer.
|
|
74
|
+
*
|
|
75
|
+
* Each rule has a {@link name}, {@link priority}, and {@link match} function.
|
|
76
|
+
* The match function receives the current token stream and a position,
|
|
77
|
+
* and returns a {@link NormalizerMatch} on success or `null` on failure.
|
|
78
|
+
*
|
|
79
|
+
* ## Priority ordering
|
|
80
|
+
* Higher priority rules are tried first at each position. This allows
|
|
81
|
+
* long phrases (priority 100, e.g. "to the power of") to match before
|
|
82
|
+
* shorter fragments (priority 80, e.g. "power of").
|
|
83
|
+
*
|
|
84
|
+
* ## Match contract
|
|
85
|
+
* - Must be pure (no side effects, no mutation of input tokens)
|
|
86
|
+
* - Must return `null` for any position that doesn't match
|
|
87
|
+
* - Consumed tokens must be consecutive starting at `pos`
|
|
88
|
+
* - Replacement tokens must be valid for downstream parsing
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* ```ts
|
|
92
|
+
* // A phrase fusion rule that converts "to the power of" into CARET
|
|
93
|
+
* const phraseRule: NormalizerRule = {
|
|
94
|
+
* name: 'phrase:to the power of',
|
|
95
|
+
* priority: 100,
|
|
96
|
+
* match: (tokens, pos) => {
|
|
97
|
+
* if (pos + 4 > tokens.length) return null;
|
|
98
|
+
* const phrase = tokens.slice(pos, pos + 5)
|
|
99
|
+
* .map(t => t.value.toLowerCase()).join(' ');
|
|
100
|
+
* if (phrase === 'to the power of') {
|
|
101
|
+
* return {
|
|
102
|
+
* consumed: 5,
|
|
103
|
+
* replacement: [createFusedToken('CARET', 'to the power of', tokens.slice(pos, pos + 5))],
|
|
104
|
+
* };
|
|
105
|
+
* }
|
|
106
|
+
* return null;
|
|
107
|
+
* },
|
|
108
|
+
* };
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
interface NormalizerRule {
|
|
112
|
+
/**
|
|
113
|
+
* Human-readable name for debugging and diagnostic display.
|
|
114
|
+
* Convention: `"category:description"`, e.g. `"phrase:to the power of"`.
|
|
115
|
+
*/
|
|
116
|
+
readonly name: string;
|
|
117
|
+
/**
|
|
118
|
+
* Priority for ordering rules. Higher values are tried first.
|
|
119
|
+
* Recommended ranges:
|
|
120
|
+
* - 100: Long multi-word phrase fusion (e.g., "to the power of")
|
|
121
|
+
* - 80: Short phrase fusion (e.g., "power of", "times by")
|
|
122
|
+
* - 50: Implicit operator insertion (e.g., implicit multiply)
|
|
123
|
+
* - 20: Domain-specific transformations
|
|
124
|
+
*/
|
|
125
|
+
readonly priority: number;
|
|
126
|
+
/**
|
|
127
|
+
* Attempt to match a pattern starting at position `pos` in the token stream.
|
|
128
|
+
*
|
|
129
|
+
* @param tokens - The current token stream (may be partially normalized from prior passes)
|
|
130
|
+
* @param pos - The current position to attempt matching from
|
|
131
|
+
* @returns A {@link NormalizerMatch} if the pattern is found, or `null` if no match
|
|
132
|
+
*/
|
|
133
|
+
match(tokens: Token[], pos: number): NormalizerMatch | null;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Record of a token fusion event performed by the normalizer.
|
|
137
|
+
*
|
|
138
|
+
* When a rule merges multiple source tokens into fewer replacement tokens,
|
|
139
|
+
* the normalizer fires a {@link NormalizerOptions.onFusion | fusion callback}
|
|
140
|
+
* with this record. The playground uses these records to render the
|
|
141
|
+
* fusion detail table showing exactly which tokens were merged and by
|
|
142
|
+
* which rule.
|
|
143
|
+
*
|
|
144
|
+
* @example
|
|
145
|
+
* ```ts
|
|
146
|
+
* // "to" "the" "power" "of" fused into CARET "^"
|
|
147
|
+
* const fusion: TokenFusion = {
|
|
148
|
+
* rule: "phrase:to the power of",
|
|
149
|
+
* sourceTokens: [toToken, theToken, powerToken, ofToken],
|
|
150
|
+
* fusedToken: caretToken,
|
|
151
|
+
* };
|
|
152
|
+
* ```
|
|
153
|
+
*/
|
|
154
|
+
interface TokenFusion {
|
|
155
|
+
/** The name of the rule that triggered this fusion (e.g., "phrase:to the power of") */
|
|
156
|
+
rule: string;
|
|
157
|
+
/** The original tokens before fusion, always ≥ 2 tokens */
|
|
158
|
+
sourceTokens: Token[];
|
|
159
|
+
/** The resulting fused token with its new type and combined value */
|
|
160
|
+
fusedToken: Token;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export type { NormalizerRule as N, TokenFusion as T, NormalizerMatch as a };
|