@lokascript/framework 2.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/LICENSE +20 -0
- package/README.md +142 -0
- package/dist/aot/aot-orchestrator.d.ts +75 -0
- package/dist/aot/aot-orchestrator.d.ts.map +1 -0
- package/dist/aot/domain-scanner.d.ts +27 -0
- package/dist/aot/domain-scanner.d.ts.map +1 -0
- package/dist/aot/index.d.ts +8 -0
- package/dist/aot/index.d.ts.map +1 -0
- package/dist/aot/types.d.ts +103 -0
- package/dist/aot/types.d.ts.map +1 -0
- package/dist/api/create-dsl.d.ts +91 -0
- package/dist/api/create-dsl.d.ts.map +1 -0
- package/dist/api/dispatcher.d.ts +108 -0
- package/dist/api/dispatcher.d.ts.map +1 -0
- package/dist/api/domain-registry.d.ts +152 -0
- package/dist/api/domain-registry.d.ts.map +1 -0
- package/dist/api/index.d.ts +7 -0
- package/dist/api/index.d.ts.map +1 -0
- package/dist/api/index.js +2082 -0
- package/dist/api/index.js.map +1 -0
- package/dist/core/index.d.ts +7 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +2674 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/logger.d.ts +32 -0
- package/dist/core/logger.d.ts.map +1 -0
- package/dist/core/pattern-matching/index.d.ts +6 -0
- package/dist/core/pattern-matching/index.d.ts.map +1 -0
- package/dist/core/pattern-matching/index.js +1239 -0
- package/dist/core/pattern-matching/index.js.map +1 -0
- package/dist/core/pattern-matching/pattern-matcher.d.ts +239 -0
- package/dist/core/pattern-matching/pattern-matcher.d.ts.map +1 -0
- package/dist/core/pattern-matching/utils/index.d.ts +6 -0
- package/dist/core/pattern-matching/utils/index.d.ts.map +1 -0
- package/dist/core/pattern-matching/utils/possessive-keywords.d.ts +38 -0
- package/dist/core/pattern-matching/utils/possessive-keywords.d.ts.map +1 -0
- package/dist/core/pattern-matching/utils/type-validation.d.ts +63 -0
- package/dist/core/pattern-matching/utils/type-validation.d.ts.map +1 -0
- package/dist/core/tokenization/base-tokenizer.d.ts +344 -0
- package/dist/core/tokenization/base-tokenizer.d.ts.map +1 -0
- package/dist/core/tokenization/char-classifiers.d.ts +56 -0
- package/dist/core/tokenization/char-classifiers.d.ts.map +1 -0
- package/dist/core/tokenization/default-extractors.d.ts +48 -0
- package/dist/core/tokenization/default-extractors.d.ts.map +1 -0
- package/dist/core/tokenization/extractors/index.d.ts +9 -0
- package/dist/core/tokenization/extractors/index.d.ts.map +1 -0
- package/dist/core/tokenization/extractors/operator.d.ts +23 -0
- package/dist/core/tokenization/extractors/operator.d.ts.map +1 -0
- package/dist/core/tokenization/extractors/punctuation.d.ts +22 -0
- package/dist/core/tokenization/extractors/punctuation.d.ts.map +1 -0
- package/dist/core/tokenization/extractors.d.ts +61 -0
- package/dist/core/tokenization/extractors.d.ts.map +1 -0
- package/dist/core/tokenization/index.d.ts +11 -0
- package/dist/core/tokenization/index.d.ts.map +1 -0
- package/dist/core/tokenization/index.js +1345 -0
- package/dist/core/tokenization/index.js.map +1 -0
- package/dist/core/tokenization/morphology/index.d.ts +5 -0
- package/dist/core/tokenization/morphology/index.d.ts.map +1 -0
- package/dist/core/tokenization/morphology/types.d.ts +110 -0
- package/dist/core/tokenization/morphology/types.d.ts.map +1 -0
- package/dist/core/tokenization/token-utils.d.ts +111 -0
- package/dist/core/tokenization/token-utils.d.ts.map +1 -0
- package/dist/core/types.d.ts +382 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +108 -0
- package/dist/core/types.js.map +1 -0
- package/dist/generation/diagnostics.d.ts +120 -0
- package/dist/generation/diagnostics.d.ts.map +1 -0
- package/dist/generation/index.d.ts +7 -0
- package/dist/generation/index.d.ts.map +1 -0
- package/dist/generation/index.js +339 -0
- package/dist/generation/index.js.map +1 -0
- package/dist/generation/pattern-generator.d.ts +48 -0
- package/dist/generation/pattern-generator.d.ts.map +1 -0
- package/dist/generation/renderer.d.ts +115 -0
- package/dist/generation/renderer.d.ts.map +1 -0
- package/dist/grammar/index.d.ts +10 -0
- package/dist/grammar/index.d.ts.map +1 -0
- package/dist/grammar/index.js +391 -0
- package/dist/grammar/index.js.map +1 -0
- package/dist/grammar/transformer.d.ts +56 -0
- package/dist/grammar/transformer.d.ts.map +1 -0
- package/dist/grammar/types.d.ts +236 -0
- package/dist/grammar/types.d.ts.map +1 -0
- package/dist/index.cjs +4454 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4336 -0
- package/dist/index.js.map +1 -0
- package/dist/interfaces/dictionary.d.ts +82 -0
- package/dist/interfaces/dictionary.d.ts.map +1 -0
- package/dist/interfaces/index.d.ts +10 -0
- package/dist/interfaces/index.d.ts.map +1 -0
- package/dist/interfaces/profile-provider.d.ts +67 -0
- package/dist/interfaces/profile-provider.d.ts.map +1 -0
- package/dist/interfaces/value-extractor.d.ts +168 -0
- package/dist/interfaces/value-extractor.d.ts.map +1 -0
- package/dist/multilingual/index.d.ts +8 -0
- package/dist/multilingual/index.d.ts.map +1 -0
- package/dist/multilingual/index.js +1 -0
- package/dist/multilingual/index.js.map +1 -0
- package/dist/parsing/index.d.ts +8 -0
- package/dist/parsing/index.d.ts.map +1 -0
- package/dist/parsing/index.js +1415 -0
- package/dist/parsing/index.js.map +1 -0
- package/dist/parsing/multi-statement.d.ts +265 -0
- package/dist/parsing/multi-statement.d.ts.map +1 -0
- package/dist/schema/command-schema.d.ts +78 -0
- package/dist/schema/command-schema.d.ts.map +1 -0
- package/dist/schema/index.d.ts +5 -0
- package/dist/schema/index.d.ts.map +1 -0
- package/dist/schema/index.js +25 -0
- package/dist/schema/index.js.map +1 -0
- package/dist/test-setup.d.ts +9 -0
- package/dist/test-setup.d.ts.map +1 -0
- package/dist/testing/index.d.ts +50 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +16969 -0
- package/dist/testing/index.js.map +1 -0
- package/package.json +122 -0
- package/src/__test__/fixtures/sql-dsl.ts +232 -0
- package/src/__test__/sql-integration.test.ts +189 -0
- package/src/__test__/test-utils.ts +260 -0
- package/src/aot/aot-orchestrator.test.ts +413 -0
- package/src/aot/aot-orchestrator.ts +238 -0
- package/src/aot/domain-scanner.ts +178 -0
- package/src/aot/index.ts +8 -0
- package/src/aot/types.ts +124 -0
- package/src/api/create-dsl.ts +367 -0
- package/src/api/dispatcher.test.ts +336 -0
- package/src/api/dispatcher.ts +222 -0
- package/src/api/domain-registry.test.ts +336 -0
- package/src/api/domain-registry.ts +500 -0
- package/src/api/index.ts +7 -0
- package/src/core/index.ts +7 -0
- package/src/core/logger.ts +130 -0
- package/src/core/pattern-matching/index.ts +6 -0
- package/src/core/pattern-matching/pattern-matcher.test.ts +900 -0
- package/src/core/pattern-matching/pattern-matcher.ts +1548 -0
- package/src/core/pattern-matching/pattern-matcher.ts.backup +1267 -0
- package/src/core/pattern-matching/utils/index.ts +6 -0
- package/src/core/pattern-matching/utils/possessive-keywords.ts +55 -0
- package/src/core/pattern-matching/utils/type-validation.test.ts +316 -0
- package/src/core/pattern-matching/utils/type-validation.ts +134 -0
- package/src/core/tokenization/base-tokenizer.ts +916 -0
- package/src/core/tokenization/char-classifiers.ts +79 -0
- package/src/core/tokenization/create-simple-tokenizer.test.ts +260 -0
- package/src/core/tokenization/default-extractors.ts +69 -0
- package/src/core/tokenization/extractors/index.ts +9 -0
- package/src/core/tokenization/extractors/operator.ts +75 -0
- package/src/core/tokenization/extractors/punctuation.ts +39 -0
- package/src/core/tokenization/extractors.ts +452 -0
- package/src/core/tokenization/index.ts +11 -0
- package/src/core/tokenization/morphology/index.ts +5 -0
- package/src/core/tokenization/morphology/types.ts +211 -0
- package/src/core/tokenization/token-utils.ts +252 -0
- package/src/core/types.ts +589 -0
- package/src/generation/diagnostics.test.ts +171 -0
- package/src/generation/diagnostics.ts +239 -0
- package/src/generation/index.ts +7 -0
- package/src/generation/pattern-generator.test.ts +430 -0
- package/src/generation/pattern-generator.ts +315 -0
- package/src/generation/renderer.test.ts +266 -0
- package/src/generation/renderer.ts +244 -0
- package/src/grammar/index.ts +12 -0
- package/src/grammar/transformer.ts +159 -0
- package/src/grammar/types.ts +630 -0
- package/src/index.ts +157 -0
- package/src/interfaces/dictionary.ts +123 -0
- package/src/interfaces/index.ts +10 -0
- package/src/interfaces/profile-provider.ts +88 -0
- package/src/interfaces/value-extractor.ts +435 -0
- package/src/multilingual/index.ts +9 -0
- package/src/parsing/index.ts +27 -0
- package/src/parsing/multi-statement.test.ts +480 -0
- package/src/parsing/multi-statement.ts +648 -0
- package/src/schema/command-schema.ts +118 -0
- package/src/schema/index.ts +5 -0
- package/src/test-setup.ts +45 -0
- package/src/testing/index.ts +137 -0
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
createMultiStatementParser,
|
|
4
|
+
accumulateBlocks,
|
|
5
|
+
type MultiStatementConfig,
|
|
6
|
+
type ParsedStatement,
|
|
7
|
+
} from './multi-statement';
|
|
8
|
+
import type { MultilingualDSL } from '../api/create-dsl';
|
|
9
|
+
import type { SemanticNode } from '../core/types';
|
|
10
|
+
|
|
11
|
+
// =============================================================================
|
|
12
|
+
// Test Helpers
|
|
13
|
+
// =============================================================================
|
|
14
|
+
|
|
15
|
+
function createMockDSL(
|
|
16
|
+
parseImpl?: (input: string, language: string) => SemanticNode
|
|
17
|
+
): MultilingualDSL {
|
|
18
|
+
const defaultParse = (input: string, _language: string): SemanticNode => ({
|
|
19
|
+
kind: 'command',
|
|
20
|
+
action: input.split(/\s+/)[0].toLowerCase(),
|
|
21
|
+
roles: new Map([['raw', { type: 'expression' as const, raw: input }]]),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
parse: parseImpl ?? defaultParse,
|
|
26
|
+
parseWithConfidence: (input: string, language: string) => ({
|
|
27
|
+
node: (parseImpl ?? defaultParse)(input, language),
|
|
28
|
+
confidence: 1.0,
|
|
29
|
+
}),
|
|
30
|
+
validate: () => ({ valid: true }),
|
|
31
|
+
compile: () => ({ ok: true }),
|
|
32
|
+
translate: (input: string) => input,
|
|
33
|
+
getSupportedLanguages: () => ['en'],
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function mockNode(action: string, source?: string): SemanticNode {
|
|
38
|
+
return {
|
|
39
|
+
kind: 'command',
|
|
40
|
+
action,
|
|
41
|
+
roles: new Map(source ? [['raw', { type: 'expression' as const, raw: source }]] : []),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// =============================================================================
|
|
46
|
+
// Statement Splitting
|
|
47
|
+
// =============================================================================
|
|
48
|
+
|
|
49
|
+
describe('createMultiStatementParser', () => {
|
|
50
|
+
describe('line-based splitting', () => {
|
|
51
|
+
const config: MultiStatementConfig = {
|
|
52
|
+
split: { mode: 'line' },
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
it('splits input on newlines', () => {
|
|
56
|
+
const dsl = createMockDSL();
|
|
57
|
+
const parser = createMultiStatementParser(dsl, config);
|
|
58
|
+
|
|
59
|
+
const result = parser.parse('given visible\nwhen click\nthen hidden', 'en');
|
|
60
|
+
expect(result.statements).toHaveLength(3);
|
|
61
|
+
expect(result.errors).toHaveLength(0);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('skips empty lines', () => {
|
|
65
|
+
const dsl = createMockDSL();
|
|
66
|
+
const parser = createMultiStatementParser(dsl, config);
|
|
67
|
+
|
|
68
|
+
const result = parser.parse('given visible\n\n\nwhen click', 'en');
|
|
69
|
+
expect(result.statements).toHaveLength(2);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('skips comment lines', () => {
|
|
73
|
+
const dsl = createMockDSL();
|
|
74
|
+
const parser = createMultiStatementParser(dsl, config);
|
|
75
|
+
|
|
76
|
+
const result = parser.parse(
|
|
77
|
+
'given visible\n-- this is a comment\n// also a comment\nwhen click',
|
|
78
|
+
'en'
|
|
79
|
+
);
|
|
80
|
+
expect(result.statements).toHaveLength(2);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('tracks line numbers correctly', () => {
|
|
84
|
+
const dsl = createMockDSL();
|
|
85
|
+
const parser = createMultiStatementParser(dsl, config);
|
|
86
|
+
|
|
87
|
+
const result = parser.parse('given visible\n\nwhen click\n-- comment\nthen hidden', 'en');
|
|
88
|
+
expect(result.statements[0].line).toBe(1);
|
|
89
|
+
expect(result.statements[1].line).toBe(3);
|
|
90
|
+
expect(result.statements[2].line).toBe(5);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('tracks indentation', () => {
|
|
94
|
+
const dsl = createMockDSL();
|
|
95
|
+
const parser = createMultiStatementParser(dsl, config);
|
|
96
|
+
|
|
97
|
+
const result = parser.parse('test login\n given visible\n expect hidden', 'en');
|
|
98
|
+
expect(result.statements[0].indent).toBe(0);
|
|
99
|
+
expect(result.statements[1].indent).toBe(2);
|
|
100
|
+
expect(result.statements[2].indent).toBe(4);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe('delimiter-based splitting', () => {
|
|
105
|
+
const config: MultiStatementConfig = {
|
|
106
|
+
split: {
|
|
107
|
+
mode: 'delimiter',
|
|
108
|
+
delimiters: {
|
|
109
|
+
en: /,\s*|\n\s*/,
|
|
110
|
+
ja: /、|。|\n\s*/,
|
|
111
|
+
},
|
|
112
|
+
defaultDelimiter: /,\s*|\n\s*/,
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
it('splits on language-specific delimiters', () => {
|
|
117
|
+
const dsl = createMockDSL();
|
|
118
|
+
const parser = createMultiStatementParser(dsl, config);
|
|
119
|
+
|
|
120
|
+
const result = parser.parse('given visible, when click, then hidden', 'en');
|
|
121
|
+
expect(result.statements).toHaveLength(3);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('uses language-specific delimiters for Japanese', () => {
|
|
125
|
+
const dsl = createMockDSL();
|
|
126
|
+
const parser = createMultiStatementParser(dsl, config);
|
|
127
|
+
|
|
128
|
+
const result = parser.parse('前提 表示、もし クリック。ならば 非表示', 'ja');
|
|
129
|
+
expect(result.statements).toHaveLength(3);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('uses default delimiter for unknown languages', () => {
|
|
133
|
+
const dsl = createMockDSL();
|
|
134
|
+
const parser = createMultiStatementParser(dsl, config);
|
|
135
|
+
|
|
136
|
+
const result = parser.parse('given visible, when click', 'fr');
|
|
137
|
+
expect(result.statements).toHaveLength(2);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// =============================================================================
|
|
142
|
+
// Keyword Classification
|
|
143
|
+
// =============================================================================
|
|
144
|
+
|
|
145
|
+
describe('keyword classification', () => {
|
|
146
|
+
const config: MultiStatementConfig = {
|
|
147
|
+
split: { mode: 'line' },
|
|
148
|
+
keywords: {
|
|
149
|
+
categories: {
|
|
150
|
+
given: { en: ['given'], ja: ['前提'], es: ['dado'] },
|
|
151
|
+
when: { en: ['when'], ja: ['もし'], es: ['cuando'] },
|
|
152
|
+
then: { en: ['then'], ja: ['ならば'], es: ['entonces'] },
|
|
153
|
+
},
|
|
154
|
+
wordOrders: { ja: 'SOV' },
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
it('classifies SVO lines by start keyword', () => {
|
|
159
|
+
const dsl = createMockDSL();
|
|
160
|
+
const parser = createMultiStatementParser(dsl, config);
|
|
161
|
+
|
|
162
|
+
const result = parser.parse('given visible\nwhen click\nthen hidden', 'en');
|
|
163
|
+
expect(result.statements[0].category).toBe('given');
|
|
164
|
+
expect(result.statements[1].category).toBe('when');
|
|
165
|
+
expect(result.statements[2].category).toBe('then');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('classifies SOV lines by end keyword', () => {
|
|
169
|
+
const dsl = createMockDSL();
|
|
170
|
+
const parser = createMultiStatementParser(dsl, config);
|
|
171
|
+
|
|
172
|
+
const result = parser.parse('表示 前提\nクリック もし', 'ja');
|
|
173
|
+
expect(result.statements[0].category).toBe('given');
|
|
174
|
+
expect(result.statements[1].category).toBe('when');
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('returns undefined category for unrecognized lines', () => {
|
|
178
|
+
const dsl = createMockDSL();
|
|
179
|
+
const parser = createMultiStatementParser(dsl, config);
|
|
180
|
+
|
|
181
|
+
const result = parser.parse('some random line', 'en');
|
|
182
|
+
expect(result.statements[0].category).toBeUndefined();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('falls back to English keywords for unknown languages', () => {
|
|
186
|
+
const dsl = createMockDSL();
|
|
187
|
+
const parser = createMultiStatementParser(dsl, config);
|
|
188
|
+
|
|
189
|
+
const result = parser.parse('given visible', 'fr');
|
|
190
|
+
expect(result.statements[0].category).toBe('given');
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// =============================================================================
|
|
195
|
+
// Continuation Resolution
|
|
196
|
+
// =============================================================================
|
|
197
|
+
|
|
198
|
+
describe('continuation resolution', () => {
|
|
199
|
+
const config: MultiStatementConfig = {
|
|
200
|
+
split: { mode: 'delimiter', defaultDelimiter: /,\s*|\n\s*/ },
|
|
201
|
+
keywords: {
|
|
202
|
+
categories: {
|
|
203
|
+
given: { en: ['given'] },
|
|
204
|
+
when: { en: ['when'] },
|
|
205
|
+
then: { en: ['then'] },
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
continuation: {
|
|
209
|
+
keywords: { en: ['and'] },
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
it('resolves "and" by re-prefixing with previous keyword', () => {
|
|
214
|
+
const parseCalls: string[] = [];
|
|
215
|
+
const dsl = createMockDSL(input => {
|
|
216
|
+
parseCalls.push(input);
|
|
217
|
+
return mockNode(input.split(/\s+/)[0].toLowerCase(), input);
|
|
218
|
+
});
|
|
219
|
+
const parser = createMultiStatementParser(dsl, config);
|
|
220
|
+
|
|
221
|
+
parser.parse('given visible, and enabled', 'en');
|
|
222
|
+
expect(parseCalls).toContain('given enabled');
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('does not resolve "and" without a previous statement', () => {
|
|
226
|
+
const parseCalls: string[] = [];
|
|
227
|
+
const dsl = createMockDSL(input => {
|
|
228
|
+
parseCalls.push(input);
|
|
229
|
+
return mockNode('test', input);
|
|
230
|
+
});
|
|
231
|
+
const parser = createMultiStatementParser(dsl, config);
|
|
232
|
+
|
|
233
|
+
const result = parser.parse('and something', 'en');
|
|
234
|
+
// Without previous category, continuation doesn't resolve — parses as-is
|
|
235
|
+
expect(parseCalls).toContain('and something');
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('supports custom resolve function', () => {
|
|
239
|
+
const customConfig: MultiStatementConfig = {
|
|
240
|
+
...config,
|
|
241
|
+
continuation: {
|
|
242
|
+
keywords: { en: ['and'] },
|
|
243
|
+
resolve: (content, prevCategory) => `${prevCategory}: ${content}`,
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const parseCalls: string[] = [];
|
|
248
|
+
const dsl = createMockDSL(input => {
|
|
249
|
+
parseCalls.push(input);
|
|
250
|
+
return mockNode('test', input);
|
|
251
|
+
});
|
|
252
|
+
const parser = createMultiStatementParser(dsl, customConfig);
|
|
253
|
+
|
|
254
|
+
parser.parse('given visible, and enabled', 'en');
|
|
255
|
+
expect(parseCalls).toContain('given: enabled');
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// =============================================================================
|
|
260
|
+
// Preprocessor
|
|
261
|
+
// =============================================================================
|
|
262
|
+
|
|
263
|
+
describe('preprocessor', () => {
|
|
264
|
+
it('allows transforming lines before parsing', () => {
|
|
265
|
+
const config: MultiStatementConfig = {
|
|
266
|
+
split: { mode: 'line' },
|
|
267
|
+
preprocessor: (line, _cat, _lang) => {
|
|
268
|
+
// Strip "the" article
|
|
269
|
+
return line.replace(/\bthe\s+/gi, '');
|
|
270
|
+
},
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
const parseCalls: string[] = [];
|
|
274
|
+
const dsl = createMockDSL(input => {
|
|
275
|
+
parseCalls.push(input);
|
|
276
|
+
return mockNode('test', input);
|
|
277
|
+
});
|
|
278
|
+
const parser = createMultiStatementParser(dsl, config);
|
|
279
|
+
|
|
280
|
+
parser.parse('toggle the .active', 'en');
|
|
281
|
+
expect(parseCalls[0]).toBe('toggle .active');
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('allows skipping lines by returning null', () => {
|
|
285
|
+
const config: MultiStatementConfig = {
|
|
286
|
+
split: { mode: 'line' },
|
|
287
|
+
preprocessor: line => {
|
|
288
|
+
if (line.startsWith('skip:')) return null;
|
|
289
|
+
return line;
|
|
290
|
+
},
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
const dsl = createMockDSL();
|
|
294
|
+
const parser = createMultiStatementParser(dsl, config);
|
|
295
|
+
|
|
296
|
+
const result = parser.parse('given visible\nskip: this line\nwhen click', 'en');
|
|
297
|
+
expect(result.statements).toHaveLength(2);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('receives context with previous statement', () => {
|
|
301
|
+
const contexts: Array<{ previous?: { action: string }; lineNumber: number }> = [];
|
|
302
|
+
|
|
303
|
+
const config: MultiStatementConfig = {
|
|
304
|
+
split: { mode: 'line' },
|
|
305
|
+
preprocessor: (line, _cat, _lang, ctx) => {
|
|
306
|
+
contexts.push({
|
|
307
|
+
previous: ctx.previous ? { action: ctx.previous.node.action } : undefined,
|
|
308
|
+
lineNumber: ctx.lineNumber,
|
|
309
|
+
});
|
|
310
|
+
return line;
|
|
311
|
+
},
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
const dsl = createMockDSL();
|
|
315
|
+
const parser = createMultiStatementParser(dsl, config);
|
|
316
|
+
|
|
317
|
+
parser.parse('given visible\nwhen click', 'en');
|
|
318
|
+
expect(contexts[0].previous).toBeUndefined();
|
|
319
|
+
expect(contexts[0].lineNumber).toBe(1);
|
|
320
|
+
expect(contexts[1].previous?.action).toBe('given');
|
|
321
|
+
expect(contexts[1].lineNumber).toBe(2);
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// =============================================================================
|
|
326
|
+
// Error Collection
|
|
327
|
+
// =============================================================================
|
|
328
|
+
|
|
329
|
+
describe('error collection', () => {
|
|
330
|
+
it('collects parse errors with line numbers', () => {
|
|
331
|
+
const dsl = createMockDSL(input => {
|
|
332
|
+
if (input.includes('bad')) throw new Error('Parse failed');
|
|
333
|
+
return mockNode('test', input);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
const config: MultiStatementConfig = {
|
|
337
|
+
split: { mode: 'line' },
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
const parser = createMultiStatementParser(dsl, config);
|
|
341
|
+
const result = parser.parse('good line\nbad line\nanother good', 'en');
|
|
342
|
+
|
|
343
|
+
expect(result.statements).toHaveLength(2);
|
|
344
|
+
expect(result.errors).toHaveLength(1);
|
|
345
|
+
expect(result.errors[0].message).toBe('Parse failed');
|
|
346
|
+
expect(result.errors[0].line).toBe(2);
|
|
347
|
+
expect(result.errors[0].source).toBe('bad line');
|
|
348
|
+
expect(result.errors[0].code).toBe('parse-error');
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it('continues parsing after errors', () => {
|
|
352
|
+
const dsl = createMockDSL(input => {
|
|
353
|
+
if (input.includes('error')) throw new Error('fail');
|
|
354
|
+
return mockNode('test', input);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
const config: MultiStatementConfig = {
|
|
358
|
+
split: { mode: 'line' },
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
const parser = createMultiStatementParser(dsl, config);
|
|
362
|
+
const result = parser.parse('ok1\nerror1\nok2\nerror2\nok3', 'en');
|
|
363
|
+
|
|
364
|
+
expect(result.statements).toHaveLength(3);
|
|
365
|
+
expect(result.errors).toHaveLength(2);
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
// =============================================================================
|
|
371
|
+
// Block Accumulator
|
|
372
|
+
// =============================================================================
|
|
373
|
+
|
|
374
|
+
describe('accumulateBlocks', () => {
|
|
375
|
+
function stmt(
|
|
376
|
+
category: string | undefined,
|
|
377
|
+
source: string,
|
|
378
|
+
line: number,
|
|
379
|
+
indent: number
|
|
380
|
+
): ParsedStatement {
|
|
381
|
+
return {
|
|
382
|
+
node: mockNode(category ?? 'unknown', source),
|
|
383
|
+
source,
|
|
384
|
+
line,
|
|
385
|
+
category,
|
|
386
|
+
indent,
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
describe('flat mode', () => {
|
|
391
|
+
it('groups statements into blocks by block-opening keywords', () => {
|
|
392
|
+
const statements = [
|
|
393
|
+
stmt('test', 'test "Login"', 1, 0),
|
|
394
|
+
stmt('given', 'given visible', 2, 2),
|
|
395
|
+
stmt('when', 'when click', 3, 2),
|
|
396
|
+
stmt('test', 'test "Logout"', 4, 0),
|
|
397
|
+
stmt('given', 'given logged in', 5, 2),
|
|
398
|
+
];
|
|
399
|
+
|
|
400
|
+
const result = accumulateBlocks(statements, {
|
|
401
|
+
blockTypes: ['test'],
|
|
402
|
+
nesting: 'flat',
|
|
403
|
+
extractName: text => text.match(/"([^"]+)"/)?.[1],
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
expect(result.blocks).toHaveLength(2);
|
|
407
|
+
expect(result.blocks[0].name).toBe('Login');
|
|
408
|
+
expect(result.blocks[0].statements).toHaveLength(3); // test + given + when
|
|
409
|
+
expect(result.blocks[1].name).toBe('Logout');
|
|
410
|
+
expect(result.blocks[1].statements).toHaveLength(2);
|
|
411
|
+
expect(result.orphans).toHaveLength(0);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it('collects orphan statements before first block', () => {
|
|
415
|
+
const statements = [
|
|
416
|
+
stmt(undefined, 'setup line', 1, 0),
|
|
417
|
+
stmt('test', 'test "First"', 2, 0),
|
|
418
|
+
stmt('given', 'given x', 3, 2),
|
|
419
|
+
];
|
|
420
|
+
|
|
421
|
+
const result = accumulateBlocks(statements, {
|
|
422
|
+
blockTypes: ['test'],
|
|
423
|
+
nesting: 'flat',
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
expect(result.blocks).toHaveLength(1);
|
|
427
|
+
expect(result.orphans).toHaveLength(1);
|
|
428
|
+
expect(result.orphans[0].source).toBe('setup line');
|
|
429
|
+
});
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
describe('indent mode', () => {
|
|
433
|
+
it('nests blocks by indentation', () => {
|
|
434
|
+
const statements = [
|
|
435
|
+
stmt('feature', 'feature "Cart"', 1, 0),
|
|
436
|
+
stmt('test', 'test "Add"', 2, 2),
|
|
437
|
+
stmt('given', 'given visible', 3, 4),
|
|
438
|
+
stmt('when', 'when click', 4, 4),
|
|
439
|
+
stmt('test', 'test "Remove"', 5, 2),
|
|
440
|
+
stmt('when', 'when click remove', 6, 4),
|
|
441
|
+
];
|
|
442
|
+
|
|
443
|
+
const result = accumulateBlocks(statements, {
|
|
444
|
+
blockTypes: ['feature', 'test'],
|
|
445
|
+
nesting: 'indent',
|
|
446
|
+
extractName: text => text.match(/"([^"]+)"/)?.[1],
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
expect(result.blocks).toHaveLength(1);
|
|
450
|
+
const feature = result.blocks[0];
|
|
451
|
+
expect(feature.name).toBe('Cart');
|
|
452
|
+
expect(feature.children).toHaveLength(2);
|
|
453
|
+
expect(feature.children[0].name).toBe('Add');
|
|
454
|
+
expect(feature.children[0].statements).toHaveLength(3); // test + given + when
|
|
455
|
+
expect(feature.children[1].name).toBe('Remove');
|
|
456
|
+
expect(result.orphans).toHaveLength(0);
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
it('handles multiple top-level blocks', () => {
|
|
460
|
+
const statements = [
|
|
461
|
+
stmt('feature', 'feature "A"', 1, 0),
|
|
462
|
+
stmt('test', 'test "A1"', 2, 2),
|
|
463
|
+
stmt('feature', 'feature "B"', 3, 0),
|
|
464
|
+
stmt('test', 'test "B1"', 4, 2),
|
|
465
|
+
];
|
|
466
|
+
|
|
467
|
+
const result = accumulateBlocks(statements, {
|
|
468
|
+
blockTypes: ['feature', 'test'],
|
|
469
|
+
nesting: 'indent',
|
|
470
|
+
extractName: text => text.match(/"([^"]+)"/)?.[1],
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
expect(result.blocks).toHaveLength(2);
|
|
474
|
+
expect(result.blocks[0].name).toBe('A');
|
|
475
|
+
expect(result.blocks[0].children).toHaveLength(1);
|
|
476
|
+
expect(result.blocks[1].name).toBe('B');
|
|
477
|
+
expect(result.blocks[1].children).toHaveLength(1);
|
|
478
|
+
});
|
|
479
|
+
});
|
|
480
|
+
});
|