@sprig-and-prose/sprig-universe 0.3.4 → 0.4.1
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/package.json +1 -1
- package/src/ast.js +51 -4
- package/src/graph.js +533 -111
- package/src/ir.js +40 -2
- package/src/parser.js +666 -57
- package/src/scanner.js +1 -0
- package/test/aliases.test.js +91 -0
- package/test/fixtures/aliases-basic.prose +7 -0
- package/test/fixtures/aliases-conflict.prose +6 -0
- package/test/fixtures/aliases-no-leak.prose +5 -0
- package/test/fixtures/aliases-shadowing.prose +11 -0
- package/test/fixtures/aliases-single-line.prose +7 -0
- package/test/fixtures/relationship-errors.prose +20 -0
- package/test/fixtures/relationship-paired.prose +28 -0
- package/test/fixtures/relationship-scoped.prose +23 -0
- package/test/fixtures/relationship-symmetric.prose +18 -0
- package/test/fixtures/relationship-usage.prose +31 -0
package/src/parser.js
CHANGED
|
@@ -4,6 +4,19 @@
|
|
|
4
4
|
|
|
5
5
|
import { mergeSpans } from './util/span.js';
|
|
6
6
|
|
|
7
|
+
const DECLARATION_KINDS = new Set([
|
|
8
|
+
'universe',
|
|
9
|
+
'anthology',
|
|
10
|
+
'series',
|
|
11
|
+
'book',
|
|
12
|
+
'chapter',
|
|
13
|
+
'concept',
|
|
14
|
+
'relates',
|
|
15
|
+
'relationship',
|
|
16
|
+
'repository',
|
|
17
|
+
'reference',
|
|
18
|
+
]);
|
|
19
|
+
|
|
7
20
|
/**
|
|
8
21
|
* @typedef {import('./ast.js').FileAST} FileAST
|
|
9
22
|
* @typedef {import('./ast.js').UniverseDecl} UniverseDecl
|
|
@@ -13,6 +26,7 @@ import { mergeSpans } from './util/span.js';
|
|
|
13
26
|
* @typedef {import('./ast.js').ChapterDecl} ChapterDecl
|
|
14
27
|
* @typedef {import('./ast.js').ConceptDecl} ConceptDecl
|
|
15
28
|
* @typedef {import('./ast.js').RelatesDecl} RelatesDecl
|
|
29
|
+
* @typedef {import('./ast.js').RelationshipDecl} RelationshipDecl
|
|
16
30
|
* @typedef {import('./ast.js').DescribeBlock} DescribeBlock
|
|
17
31
|
* @typedef {import('./ast.js').UnknownBlock} UnknownBlock
|
|
18
32
|
* @typedef {import('./ast.js').ReferencesBlock} ReferencesBlock
|
|
@@ -48,6 +62,10 @@ class Parser {
|
|
|
48
62
|
this.file = file;
|
|
49
63
|
this.sourceText = sourceText;
|
|
50
64
|
this.pos = 0;
|
|
65
|
+
this.aliasScopes = [new Map()];
|
|
66
|
+
this.aliasDeclarations = [];
|
|
67
|
+
this.relationshipScopes = [new Map()];
|
|
68
|
+
this.relationshipDeclarations = [];
|
|
51
69
|
}
|
|
52
70
|
|
|
53
71
|
/**
|
|
@@ -100,6 +118,205 @@ class Parser {
|
|
|
100
118
|
return token;
|
|
101
119
|
}
|
|
102
120
|
|
|
121
|
+
/**
|
|
122
|
+
* @param {string} [value]
|
|
123
|
+
* @returns {Token}
|
|
124
|
+
* @throws {Error}
|
|
125
|
+
*/
|
|
126
|
+
expectIdentifierOrKeyword(value) {
|
|
127
|
+
const token = this.advance();
|
|
128
|
+
if (!token || (token.type !== 'KEYWORD' && token.type !== 'IDENTIFIER')) {
|
|
129
|
+
throw new Error(
|
|
130
|
+
`Expected identifier or keyword, got ${token ? token.type : 'EOF'} at ${this.file}:${token ? token.span.start.line : '?'}`,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
if (value !== undefined && token.value !== value) {
|
|
134
|
+
throw new Error(
|
|
135
|
+
`Expected identifier or keyword "${value}", got "${token.value}" at ${this.file}:${token.span.start.line}`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
return token;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* @param {string} value
|
|
143
|
+
* @returns {Token}
|
|
144
|
+
* @throws {Error}
|
|
145
|
+
*/
|
|
146
|
+
expectKindToken(value) {
|
|
147
|
+
return this.expectIdentifierOrKeyword(value);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
pushAliasScope() {
|
|
151
|
+
const parent = this.aliasScopes[this.aliasScopes.length - 1] || new Map();
|
|
152
|
+
this.aliasScopes.push(new Map(parent));
|
|
153
|
+
this.aliasDeclarations.push(new Map());
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* @returns {Map<string, string>}
|
|
158
|
+
*/
|
|
159
|
+
popAliasScope() {
|
|
160
|
+
this.aliasScopes.pop();
|
|
161
|
+
return this.aliasDeclarations.pop() || new Map();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* @returns {Map<string, string>}
|
|
166
|
+
*/
|
|
167
|
+
currentAliasMap() {
|
|
168
|
+
return this.aliasScopes[this.aliasScopes.length - 1] || new Map();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
pushRelationshipScope() {
|
|
172
|
+
const parent = this.relationshipScopes[this.relationshipScopes.length - 1] || new Map();
|
|
173
|
+
this.relationshipScopes.push(new Map(parent));
|
|
174
|
+
this.relationshipDeclarations.push(new Map());
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* @returns {Map<string, RelationshipDecl>}
|
|
179
|
+
*/
|
|
180
|
+
popRelationshipScope() {
|
|
181
|
+
this.relationshipScopes.pop();
|
|
182
|
+
return this.relationshipDeclarations.pop() || new Map();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* @returns {Map<string, RelationshipDecl>}
|
|
187
|
+
*/
|
|
188
|
+
currentRelationshipMap() {
|
|
189
|
+
return this.relationshipScopes[this.relationshipScopes.length - 1] || new Map();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* @param {RelationshipDecl} relationshipDecl
|
|
194
|
+
*/
|
|
195
|
+
declareRelationship(relationshipDecl) {
|
|
196
|
+
const declaredMap = this.relationshipDeclarations[this.relationshipDeclarations.length - 1];
|
|
197
|
+
if (!declaredMap) {
|
|
198
|
+
throw new Error(
|
|
199
|
+
`Relationship must be declared inside a container at ${this.file}:${relationshipDecl.source.start.line}`,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Get identifiers to check for duplicates
|
|
204
|
+
const identifiers = [];
|
|
205
|
+
if (relationshipDecl.type === 'symmetric' && relationshipDecl.id) {
|
|
206
|
+
identifiers.push(relationshipDecl.id);
|
|
207
|
+
} else if (relationshipDecl.type === 'paired') {
|
|
208
|
+
if (relationshipDecl.leftId) identifiers.push(relationshipDecl.leftId);
|
|
209
|
+
if (relationshipDecl.rightId) identifiers.push(relationshipDecl.rightId);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
for (const id of identifiers) {
|
|
213
|
+
if (declaredMap.has(id)) {
|
|
214
|
+
throw new Error(
|
|
215
|
+
`Duplicate relationship identifier "${id}" in this scope at ${this.file}:${relationshipDecl.source.start.line}`,
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
const relationshipMap = this.currentRelationshipMap();
|
|
219
|
+
relationshipMap.set(id, relationshipDecl);
|
|
220
|
+
declaredMap.set(id, relationshipDecl);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* @param {Token} nameToken
|
|
226
|
+
* @param {Token} targetToken
|
|
227
|
+
*/
|
|
228
|
+
declareAlias(nameToken, targetToken) {
|
|
229
|
+
const aliasName = nameToken.value;
|
|
230
|
+
const targetKind = targetToken.value;
|
|
231
|
+
const declaredMap = this.aliasDeclarations[this.aliasDeclarations.length - 1];
|
|
232
|
+
if (!declaredMap) {
|
|
233
|
+
throw new Error(
|
|
234
|
+
`Alias "${aliasName}" must be declared inside a container at ${this.file}:${nameToken.span.start.line}`,
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
if (DECLARATION_KINDS.has(aliasName)) {
|
|
238
|
+
throw new Error(
|
|
239
|
+
`Alias "${aliasName}" conflicts with built-in kind "${aliasName}" at ${this.file}:${nameToken.span.start.line}`,
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
if (!DECLARATION_KINDS.has(targetKind)) {
|
|
243
|
+
throw new Error(
|
|
244
|
+
`Unknown alias target kind "${targetKind}" at ${this.file}:${targetToken.span.start.line}`,
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
if (declaredMap.has(aliasName)) {
|
|
248
|
+
throw new Error(
|
|
249
|
+
`Duplicate alias "${aliasName}" in this scope at ${this.file}:${nameToken.span.start.line}`,
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
const aliasMap = this.currentAliasMap();
|
|
253
|
+
aliasMap.set(aliasName, targetKind);
|
|
254
|
+
declaredMap.set(aliasName, targetKind);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
parseAliasStatement() {
|
|
258
|
+
this.expectIdentifierOrKeyword('alias');
|
|
259
|
+
const nameToken = this.expectIdentifierOrKeyword();
|
|
260
|
+
this.expect('LBRACE');
|
|
261
|
+
const targetToken = this.expectIdentifierOrKeyword();
|
|
262
|
+
this.expect('RBRACE');
|
|
263
|
+
this.declareAlias(nameToken, targetToken);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
parseAliasesBlock() {
|
|
267
|
+
this.expectIdentifierOrKeyword('aliases');
|
|
268
|
+
this.expect('LBRACE');
|
|
269
|
+
while (!this.match('RBRACE') && !this.match('EOF')) {
|
|
270
|
+
if (!this.match('IDENTIFIER') && !this.match('KEYWORD')) {
|
|
271
|
+
const token = this.peek();
|
|
272
|
+
throw new Error(
|
|
273
|
+
`Expected alias name in aliases block at ${this.file}:${token ? token.span.start.line : '?'}`,
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
const nameToken = this.expectIdentifierOrKeyword();
|
|
277
|
+
this.expect('LBRACE');
|
|
278
|
+
const targetToken = this.expectIdentifierOrKeyword();
|
|
279
|
+
this.expect('RBRACE');
|
|
280
|
+
this.declareAlias(nameToken, targetToken);
|
|
281
|
+
}
|
|
282
|
+
this.expect('RBRACE');
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* @param {string} resolvedKind
|
|
287
|
+
* @param {string} spelledKind
|
|
288
|
+
*/
|
|
289
|
+
parseDeclarationByKind(resolvedKind, spelledKind) {
|
|
290
|
+
if (resolvedKind === 'anthology') {
|
|
291
|
+
return this.parseAnthology(spelledKind);
|
|
292
|
+
}
|
|
293
|
+
if (resolvedKind === 'series') {
|
|
294
|
+
return this.parseSeries(spelledKind);
|
|
295
|
+
}
|
|
296
|
+
if (resolvedKind === 'book') {
|
|
297
|
+
return this.parseBook(spelledKind);
|
|
298
|
+
}
|
|
299
|
+
if (resolvedKind === 'chapter') {
|
|
300
|
+
return this.parseChapter(spelledKind);
|
|
301
|
+
}
|
|
302
|
+
if (resolvedKind === 'concept') {
|
|
303
|
+
return this.parseConcept(spelledKind);
|
|
304
|
+
}
|
|
305
|
+
if (resolvedKind === 'relates') {
|
|
306
|
+
return this.parseRelates(spelledKind);
|
|
307
|
+
}
|
|
308
|
+
if (resolvedKind === 'repository') {
|
|
309
|
+
return this.parseRepository(spelledKind);
|
|
310
|
+
}
|
|
311
|
+
if (resolvedKind === 'reference') {
|
|
312
|
+
return this.parseReferenceDecl(spelledKind);
|
|
313
|
+
}
|
|
314
|
+
if (resolvedKind === 'relationship') {
|
|
315
|
+
return this.parseRelationship(spelledKind);
|
|
316
|
+
}
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
|
|
103
320
|
/**
|
|
104
321
|
* @returns {FileAST}
|
|
105
322
|
*/
|
|
@@ -174,16 +391,40 @@ class Parser {
|
|
|
174
391
|
/**
|
|
175
392
|
* @returns {UniverseDecl}
|
|
176
393
|
*/
|
|
177
|
-
parseUniverse() {
|
|
178
|
-
const startToken = this.
|
|
394
|
+
parseUniverse(spelledKind = 'universe') {
|
|
395
|
+
const startToken = this.expectKindToken(spelledKind);
|
|
179
396
|
const nameToken = this.expect('IDENTIFIER');
|
|
180
397
|
const lbrace = this.expect('LBRACE');
|
|
181
|
-
|
|
398
|
+
this.pushAliasScope();
|
|
399
|
+
this.pushRelationshipScope();
|
|
400
|
+
const body = this.parseBlockBody(
|
|
401
|
+
[
|
|
402
|
+
'anthology',
|
|
403
|
+
'series',
|
|
404
|
+
'book',
|
|
405
|
+
'chapter',
|
|
406
|
+
'concept',
|
|
407
|
+
'relates',
|
|
408
|
+
'relationship',
|
|
409
|
+
'describe',
|
|
410
|
+
'title',
|
|
411
|
+
'repository',
|
|
412
|
+
'reference',
|
|
413
|
+
'references',
|
|
414
|
+
'ordering',
|
|
415
|
+
'documentation',
|
|
416
|
+
],
|
|
417
|
+
{ allowAliases: true },
|
|
418
|
+
);
|
|
182
419
|
const rbrace = this.expect('RBRACE');
|
|
420
|
+
const aliases = this.popAliasScope();
|
|
421
|
+
const relationships = this.popRelationshipScope();
|
|
183
422
|
|
|
184
423
|
return {
|
|
185
424
|
kind: 'universe',
|
|
425
|
+
spelledKind,
|
|
186
426
|
name: nameToken.value,
|
|
427
|
+
aliases: aliases.size > 0 ? Object.fromEntries(aliases) : undefined,
|
|
187
428
|
body,
|
|
188
429
|
source: {
|
|
189
430
|
file: this.file,
|
|
@@ -195,10 +436,12 @@ class Parser {
|
|
|
195
436
|
|
|
196
437
|
/**
|
|
197
438
|
* @param {string[]} allowedKeywords - Keywords allowed in this body
|
|
439
|
+
* @param {{ allowAliases?: boolean }} [options]
|
|
198
440
|
* @returns {Array<SeriesDecl | BookDecl | ChapterDecl | ConceptDecl | RelatesDecl | DescribeBlock | TitleBlock | FromBlock | RelationshipsBlock | ReferencesBlock | DocumentationBlock | RepositoryDecl | ReferenceDecl | NamedDocumentBlock | UnknownBlock>}
|
|
199
441
|
*/
|
|
200
|
-
parseBlockBody(allowedKeywords) {
|
|
442
|
+
parseBlockBody(allowedKeywords, options = {}) {
|
|
201
443
|
const body = [];
|
|
444
|
+
const allowAliases = options.allowAliases === true;
|
|
202
445
|
|
|
203
446
|
while (!this.match('RBRACE') && !this.match('EOF')) {
|
|
204
447
|
// Check for keywords or identifiers that might start a block
|
|
@@ -206,6 +449,15 @@ class Parser {
|
|
|
206
449
|
const keyword = this.peek()?.value;
|
|
207
450
|
if (!keyword) break;
|
|
208
451
|
|
|
452
|
+
if (allowAliases && keyword === 'alias') {
|
|
453
|
+
this.parseAliasStatement();
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
if (allowAliases && keyword === 'aliases') {
|
|
457
|
+
this.parseAliasesBlock();
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
|
|
209
461
|
// Check for named document: document <IDENTIFIER> { ... }
|
|
210
462
|
if (keyword === 'document') {
|
|
211
463
|
const nextPos = this.pos + 1;
|
|
@@ -221,19 +473,16 @@ class Parser {
|
|
|
221
473
|
// Fall through to unknown block handling
|
|
222
474
|
}
|
|
223
475
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
} else if (keyword === 'relates' && allowedKeywords.includes('relates')) {
|
|
235
|
-
body.push(this.parseRelates());
|
|
236
|
-
} else if (keyword === 'describe' && allowedKeywords.includes('describe')) {
|
|
476
|
+
const resolvedKind = this.currentAliasMap().get(keyword) || keyword;
|
|
477
|
+
if (allowedKeywords.includes(resolvedKind)) {
|
|
478
|
+
const decl = this.parseDeclarationByKind(resolvedKind, keyword);
|
|
479
|
+
if (decl) {
|
|
480
|
+
body.push(decl);
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (keyword === 'describe' && allowedKeywords.includes('describe')) {
|
|
237
486
|
body.push(this.parseDescribe());
|
|
238
487
|
} else if (keyword === 'title' && allowedKeywords.includes('title')) {
|
|
239
488
|
body.push(this.parseTitle());
|
|
@@ -274,8 +523,8 @@ class Parser {
|
|
|
274
523
|
/**
|
|
275
524
|
* @returns {AnthologyDecl}
|
|
276
525
|
*/
|
|
277
|
-
parseAnthology() {
|
|
278
|
-
const startToken = this.
|
|
526
|
+
parseAnthology(spelledKind = 'anthology') {
|
|
527
|
+
const startToken = this.expectKindToken(spelledKind);
|
|
279
528
|
const nameToken = this.expect('IDENTIFIER');
|
|
280
529
|
let parentName = undefined;
|
|
281
530
|
if (this.match('KEYWORD') && this.peek()?.value === 'in') {
|
|
@@ -284,26 +533,37 @@ class Parser {
|
|
|
284
533
|
parentName = parentToken.value;
|
|
285
534
|
}
|
|
286
535
|
const lbrace = this.expect('LBRACE');
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
536
|
+
this.pushAliasScope();
|
|
537
|
+
this.pushRelationshipScope();
|
|
538
|
+
const body = this.parseBlockBody(
|
|
539
|
+
[
|
|
540
|
+
'describe',
|
|
541
|
+
'title',
|
|
542
|
+
'series',
|
|
543
|
+
'book',
|
|
544
|
+
'chapter',
|
|
545
|
+
'concept',
|
|
546
|
+
'relationship',
|
|
547
|
+
'relates',
|
|
548
|
+
'relationship',
|
|
549
|
+
'references',
|
|
550
|
+
'ordering',
|
|
551
|
+
'documentation',
|
|
552
|
+
'repository',
|
|
553
|
+
'reference',
|
|
554
|
+
],
|
|
555
|
+
{ allowAliases: true },
|
|
556
|
+
);
|
|
301
557
|
const rbrace = this.expect('RBRACE');
|
|
558
|
+
const aliases = this.popAliasScope();
|
|
559
|
+
const relationships = this.popRelationshipScope();
|
|
302
560
|
|
|
303
561
|
return {
|
|
304
562
|
kind: 'anthology',
|
|
563
|
+
spelledKind,
|
|
305
564
|
name: nameToken.value,
|
|
306
565
|
parentName,
|
|
566
|
+
aliases: aliases.size > 0 ? Object.fromEntries(aliases) : undefined,
|
|
307
567
|
body,
|
|
308
568
|
source: {
|
|
309
569
|
file: this.file,
|
|
@@ -316,8 +576,8 @@ class Parser {
|
|
|
316
576
|
/**
|
|
317
577
|
* @returns {SeriesDecl}
|
|
318
578
|
*/
|
|
319
|
-
parseSeries() {
|
|
320
|
-
const startToken = this.
|
|
579
|
+
parseSeries(spelledKind = 'series') {
|
|
580
|
+
const startToken = this.expectKindToken(spelledKind);
|
|
321
581
|
const nameToken = this.expect('IDENTIFIER');
|
|
322
582
|
|
|
323
583
|
// Optional "in <AnthologyName>" syntax
|
|
@@ -329,13 +589,20 @@ class Parser {
|
|
|
329
589
|
}
|
|
330
590
|
|
|
331
591
|
const lbrace = this.expect('LBRACE');
|
|
332
|
-
|
|
592
|
+
this.pushAliasScope();
|
|
593
|
+
const body = this.parseBlockBody(
|
|
594
|
+
['book', 'chapter', 'concept', 'describe', 'title', 'references', 'ordering', 'documentation', 'repository', 'reference'],
|
|
595
|
+
{ allowAliases: true },
|
|
596
|
+
);
|
|
333
597
|
const rbrace = this.expect('RBRACE');
|
|
598
|
+
const aliases = this.popAliasScope();
|
|
334
599
|
|
|
335
600
|
return {
|
|
336
601
|
kind: 'series',
|
|
602
|
+
spelledKind,
|
|
337
603
|
name: nameToken.value,
|
|
338
604
|
parentName,
|
|
605
|
+
aliases: aliases.size > 0 ? Object.fromEntries(aliases) : undefined,
|
|
339
606
|
body,
|
|
340
607
|
source: {
|
|
341
608
|
file: this.file,
|
|
@@ -348,8 +615,8 @@ class Parser {
|
|
|
348
615
|
/**
|
|
349
616
|
* @returns {BookDecl}
|
|
350
617
|
*/
|
|
351
|
-
parseBook() {
|
|
352
|
-
const startToken = this.
|
|
618
|
+
parseBook(spelledKind = 'book') {
|
|
619
|
+
const startToken = this.expectKindToken(spelledKind);
|
|
353
620
|
const nameToken = this.expect('IDENTIFIER');
|
|
354
621
|
|
|
355
622
|
// Optional "in <ParentName>" syntax
|
|
@@ -361,13 +628,20 @@ class Parser {
|
|
|
361
628
|
}
|
|
362
629
|
|
|
363
630
|
const lbrace = this.expect('LBRACE');
|
|
364
|
-
|
|
631
|
+
this.pushAliasScope();
|
|
632
|
+
const body = this.parseBlockBody(
|
|
633
|
+
['series', 'chapter', 'concept', 'describe', 'title', 'relationships', 'references', 'ordering', 'documentation', 'repository', 'reference'],
|
|
634
|
+
{ allowAliases: true },
|
|
635
|
+
);
|
|
365
636
|
const rbrace = this.expect('RBRACE');
|
|
637
|
+
const aliases = this.popAliasScope();
|
|
366
638
|
|
|
367
639
|
return {
|
|
368
640
|
kind: 'book',
|
|
641
|
+
spelledKind,
|
|
369
642
|
name: nameToken.value,
|
|
370
643
|
parentName,
|
|
644
|
+
aliases: aliases.size > 0 ? Object.fromEntries(aliases) : undefined,
|
|
371
645
|
body,
|
|
372
646
|
source: {
|
|
373
647
|
file: this.file,
|
|
@@ -380,29 +654,42 @@ class Parser {
|
|
|
380
654
|
/**
|
|
381
655
|
* @returns {ChapterDecl}
|
|
382
656
|
*/
|
|
383
|
-
parseChapter() {
|
|
384
|
-
const startToken = this.
|
|
657
|
+
parseChapter(spelledKind = 'chapter') {
|
|
658
|
+
const startToken = this.expectKindToken(spelledKind);
|
|
385
659
|
const nameToken = this.expect('IDENTIFIER');
|
|
386
660
|
|
|
387
|
-
// Chapters must belong to a book - check for "in" keyword
|
|
388
|
-
|
|
661
|
+
// Chapters must belong to a book - check for "in" keyword or nested structure
|
|
662
|
+
let parentName = undefined;
|
|
663
|
+
if (this.match('KEYWORD') && this.peek()?.value === 'in') {
|
|
664
|
+
// Explicit "in" clause
|
|
665
|
+
this.expect('KEYWORD', 'in');
|
|
666
|
+
const parentToken = this.expect('IDENTIFIER');
|
|
667
|
+
parentName = parentToken.value;
|
|
668
|
+
} else if (!this.match('LBRACE')) {
|
|
669
|
+
// Neither "in" nor nested - error
|
|
389
670
|
const nextToken = this.peek();
|
|
390
671
|
const line = nextToken ? nextToken.span.start.line : startToken.span.start.line;
|
|
391
672
|
throw new Error(
|
|
392
673
|
`Chapter "${nameToken.value}" must belong to a book. Use "chapter ${nameToken.value} in <BookName> { ... }" at ${this.file}:${line}`,
|
|
393
674
|
);
|
|
394
675
|
}
|
|
676
|
+
// If next token is LBRACE, it's nested - parentName stays undefined
|
|
395
677
|
|
|
396
|
-
this.expect('KEYWORD', 'in');
|
|
397
|
-
const parentToken = this.expect('IDENTIFIER');
|
|
398
678
|
const lbrace = this.expect('LBRACE');
|
|
399
|
-
|
|
679
|
+
this.pushAliasScope();
|
|
680
|
+
const body = this.parseBlockBody(
|
|
681
|
+
['concept', 'describe', 'title', 'references', 'relationships', 'documentation', 'repository', 'reference'],
|
|
682
|
+
{ allowAliases: true },
|
|
683
|
+
);
|
|
400
684
|
const rbrace = this.expect('RBRACE');
|
|
685
|
+
const aliases = this.popAliasScope();
|
|
401
686
|
|
|
402
687
|
return {
|
|
403
688
|
kind: 'chapter',
|
|
689
|
+
spelledKind,
|
|
404
690
|
name: nameToken.value,
|
|
405
|
-
parentName
|
|
691
|
+
parentName,
|
|
692
|
+
aliases: aliases.size > 0 ? Object.fromEntries(aliases) : undefined,
|
|
406
693
|
body,
|
|
407
694
|
source: {
|
|
408
695
|
file: this.file,
|
|
@@ -415,8 +702,8 @@ class Parser {
|
|
|
415
702
|
/**
|
|
416
703
|
* @returns {ConceptDecl}
|
|
417
704
|
*/
|
|
418
|
-
parseConcept() {
|
|
419
|
-
const startToken = this.
|
|
705
|
+
parseConcept(spelledKind = 'concept') {
|
|
706
|
+
const startToken = this.expectKindToken(spelledKind);
|
|
420
707
|
const nameToken = this.expect('IDENTIFIER');
|
|
421
708
|
|
|
422
709
|
// Optional "in <ParentName>" syntax
|
|
@@ -428,13 +715,20 @@ class Parser {
|
|
|
428
715
|
}
|
|
429
716
|
|
|
430
717
|
const lbrace = this.expect('LBRACE');
|
|
431
|
-
|
|
718
|
+
this.pushAliasScope();
|
|
719
|
+
const body = this.parseBlockBody(
|
|
720
|
+
['describe', 'title', 'references', 'relationships', 'ordering', 'documentation', 'repository', 'reference'],
|
|
721
|
+
{ allowAliases: true },
|
|
722
|
+
);
|
|
432
723
|
const rbrace = this.expect('RBRACE');
|
|
724
|
+
const aliases = this.popAliasScope();
|
|
433
725
|
|
|
434
726
|
return {
|
|
435
727
|
kind: 'concept',
|
|
728
|
+
spelledKind,
|
|
436
729
|
name: nameToken.value,
|
|
437
730
|
parentName,
|
|
731
|
+
aliases: aliases.size > 0 ? Object.fromEntries(aliases) : undefined,
|
|
438
732
|
body,
|
|
439
733
|
source: {
|
|
440
734
|
file: this.file,
|
|
@@ -447,19 +741,23 @@ class Parser {
|
|
|
447
741
|
/**
|
|
448
742
|
* @returns {RelatesDecl}
|
|
449
743
|
*/
|
|
450
|
-
parseRelates() {
|
|
451
|
-
const startToken = this.
|
|
744
|
+
parseRelates(spelledKind = 'relates') {
|
|
745
|
+
const startToken = this.expectKindToken(spelledKind);
|
|
452
746
|
const aToken = this.expect('IDENTIFIER');
|
|
453
747
|
this.expect('KEYWORD', 'and');
|
|
454
748
|
const bToken = this.expect('IDENTIFIER');
|
|
455
749
|
const lbrace = this.expect('LBRACE');
|
|
456
|
-
|
|
750
|
+
this.pushAliasScope();
|
|
751
|
+
const body = this.parseBlockBody(['describe', 'title', 'from', 'relationships'], { allowAliases: true });
|
|
457
752
|
const rbrace = this.expect('RBRACE');
|
|
753
|
+
const aliases = this.popAliasScope();
|
|
458
754
|
|
|
459
755
|
return {
|
|
460
756
|
kind: 'relates',
|
|
757
|
+
spelledKind,
|
|
461
758
|
a: aToken.value,
|
|
462
759
|
b: bToken.value,
|
|
760
|
+
aliases: aliases.size > 0 ? Object.fromEntries(aliases) : undefined,
|
|
463
761
|
body,
|
|
464
762
|
source: {
|
|
465
763
|
file: this.file,
|
|
@@ -469,6 +767,191 @@ class Parser {
|
|
|
469
767
|
};
|
|
470
768
|
}
|
|
471
769
|
|
|
770
|
+
/**
|
|
771
|
+
* Parses a relationship declaration (symmetric or paired)
|
|
772
|
+
* @returns {RelationshipDecl}
|
|
773
|
+
*/
|
|
774
|
+
parseRelationship(spelledKind = 'relationship') {
|
|
775
|
+
const startToken = this.expectKindToken(spelledKind);
|
|
776
|
+
const firstIdToken = this.expectIdentifierOrKeyword();
|
|
777
|
+
|
|
778
|
+
// Check if this is paired (has "and" after first identifier)
|
|
779
|
+
if (this.match('KEYWORD') && this.peek()?.value === 'and') {
|
|
780
|
+
// Paired relationship: relationship <leftId> and <rightId> { ... }
|
|
781
|
+
return this.parseRelationshipPaired(startToken, firstIdToken, spelledKind);
|
|
782
|
+
} else {
|
|
783
|
+
// Symmetric relationship: relationship <id> { ... }
|
|
784
|
+
return this.parseRelationshipSymmetric(startToken, firstIdToken, spelledKind);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* Parses a symmetric relationship declaration
|
|
790
|
+
* @param {Token} startToken
|
|
791
|
+
* @param {Token} idToken
|
|
792
|
+
* @param {string} spelledKind
|
|
793
|
+
* @returns {RelationshipDecl}
|
|
794
|
+
*/
|
|
795
|
+
parseRelationshipSymmetric(startToken, idToken, spelledKind) {
|
|
796
|
+
const lbrace = this.expect('LBRACE');
|
|
797
|
+
let describe = null;
|
|
798
|
+
let label = null;
|
|
799
|
+
|
|
800
|
+
while (!this.match('RBRACE') && !this.match('EOF')) {
|
|
801
|
+
if (this.match('KEYWORD') || this.match('IDENTIFIER')) {
|
|
802
|
+
const keyword = this.peek()?.value;
|
|
803
|
+
if (keyword === 'describe') {
|
|
804
|
+
if (describe !== null) {
|
|
805
|
+
throw new Error(
|
|
806
|
+
`Multiple describe blocks in relationship "${idToken.value}" at ${this.file}:${startToken.span.start.line}`,
|
|
807
|
+
);
|
|
808
|
+
}
|
|
809
|
+
describe = this.parseDescribe();
|
|
810
|
+
} else if (keyword === 'label') {
|
|
811
|
+
if (label !== null) {
|
|
812
|
+
throw new Error(
|
|
813
|
+
`Multiple label blocks in relationship "${idToken.value}" at ${this.file}:${startToken.span.start.line}`,
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
label = this.parseStringBlock('label');
|
|
817
|
+
} else {
|
|
818
|
+
this.advance();
|
|
819
|
+
}
|
|
820
|
+
} else {
|
|
821
|
+
this.advance();
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
const rbrace = this.expect('RBRACE');
|
|
826
|
+
|
|
827
|
+
const relationshipDecl = {
|
|
828
|
+
kind: 'relationship',
|
|
829
|
+
spelledKind,
|
|
830
|
+
type: 'symmetric',
|
|
831
|
+
id: idToken.value,
|
|
832
|
+
describe: describe || undefined,
|
|
833
|
+
label: label || undefined,
|
|
834
|
+
source: {
|
|
835
|
+
file: this.file,
|
|
836
|
+
start: startToken.span.start,
|
|
837
|
+
end: rbrace.span.end,
|
|
838
|
+
},
|
|
839
|
+
};
|
|
840
|
+
|
|
841
|
+
// Declare the relationship in current scope
|
|
842
|
+
this.declareRelationship(relationshipDecl);
|
|
843
|
+
|
|
844
|
+
return relationshipDecl;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
/**
|
|
848
|
+
* Parses a paired (directional) relationship declaration
|
|
849
|
+
* @param {Token} startToken
|
|
850
|
+
* @param {Token} leftIdToken
|
|
851
|
+
* @param {string} spelledKind
|
|
852
|
+
* @returns {RelationshipDecl}
|
|
853
|
+
*/
|
|
854
|
+
parseRelationshipPaired(startToken, leftIdToken, spelledKind) {
|
|
855
|
+
this.expect('KEYWORD', 'and');
|
|
856
|
+
const rightIdToken = this.expectIdentifierOrKeyword();
|
|
857
|
+
const lbrace = this.expect('LBRACE');
|
|
858
|
+
|
|
859
|
+
let pairDescribe = null;
|
|
860
|
+
const fromBlocks = {};
|
|
861
|
+
|
|
862
|
+
while (!this.match('RBRACE') && !this.match('EOF')) {
|
|
863
|
+
if (this.match('KEYWORD') || this.match('IDENTIFIER')) {
|
|
864
|
+
const keyword = this.peek()?.value;
|
|
865
|
+
if (keyword === 'describe') {
|
|
866
|
+
if (pairDescribe !== null) {
|
|
867
|
+
throw new Error(
|
|
868
|
+
`Multiple describe blocks in relationship "${leftIdToken.value} and ${rightIdToken.value}" at ${this.file}:${startToken.span.start.line}`,
|
|
869
|
+
);
|
|
870
|
+
}
|
|
871
|
+
pairDescribe = this.parseDescribe();
|
|
872
|
+
} else if (keyword === 'from') {
|
|
873
|
+
const fromStartToken = this.advance();
|
|
874
|
+
const endpointToken = this.expect('IDENTIFIER');
|
|
875
|
+
|
|
876
|
+
// Validate: from block must reference leftId or rightId
|
|
877
|
+
if (endpointToken.value !== leftIdToken.value && endpointToken.value !== rightIdToken.value) {
|
|
878
|
+
throw new Error(
|
|
879
|
+
`from block references "${endpointToken.value}" which is not declared in relationship "${leftIdToken.value} and ${rightIdToken.value}" at ${this.file}:${endpointToken.span.start.line}`,
|
|
880
|
+
);
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
const fromLbrace = this.expect('LBRACE');
|
|
884
|
+
let fromLabel = null;
|
|
885
|
+
let fromDescribe = null;
|
|
886
|
+
|
|
887
|
+
// Parse from block body (label and describe)
|
|
888
|
+
while (!this.match('RBRACE') && !this.match('EOF')) {
|
|
889
|
+
if (this.match('KEYWORD') || this.match('IDENTIFIER')) {
|
|
890
|
+
const fromKeyword = this.peek()?.value;
|
|
891
|
+
if (fromKeyword === 'label') {
|
|
892
|
+
if (fromLabel !== null) {
|
|
893
|
+
throw new Error(
|
|
894
|
+
`Multiple label blocks in from "${endpointToken.value}" at ${this.file}:${endpointToken.span.start.line}`,
|
|
895
|
+
);
|
|
896
|
+
}
|
|
897
|
+
fromLabel = this.parseStringBlock('label');
|
|
898
|
+
} else if (fromKeyword === 'describe') {
|
|
899
|
+
if (fromDescribe !== null) {
|
|
900
|
+
throw new Error(
|
|
901
|
+
`Multiple describe blocks in from "${endpointToken.value}" at ${this.file}:${endpointToken.span.start.line}`,
|
|
902
|
+
);
|
|
903
|
+
}
|
|
904
|
+
fromDescribe = this.parseDescribe();
|
|
905
|
+
} else {
|
|
906
|
+
this.advance();
|
|
907
|
+
}
|
|
908
|
+
} else {
|
|
909
|
+
this.advance();
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
const fromRbrace = this.expect('RBRACE');
|
|
914
|
+
|
|
915
|
+
const fromSide = {};
|
|
916
|
+
if (fromLabel !== null) {
|
|
917
|
+
fromSide.label = fromLabel;
|
|
918
|
+
}
|
|
919
|
+
if (fromDescribe !== null) {
|
|
920
|
+
fromSide.describe = fromDescribe;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
fromBlocks[endpointToken.value] = fromSide;
|
|
924
|
+
} else {
|
|
925
|
+
this.advance();
|
|
926
|
+
}
|
|
927
|
+
} else {
|
|
928
|
+
this.advance();
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
const rbrace = this.expect('RBRACE');
|
|
933
|
+
|
|
934
|
+
const relationshipDecl = {
|
|
935
|
+
kind: 'relationship',
|
|
936
|
+
spelledKind,
|
|
937
|
+
type: 'paired',
|
|
938
|
+
leftId: leftIdToken.value,
|
|
939
|
+
rightId: rightIdToken.value,
|
|
940
|
+
describe: pairDescribe || undefined,
|
|
941
|
+
from: Object.keys(fromBlocks).length > 0 ? fromBlocks : undefined,
|
|
942
|
+
source: {
|
|
943
|
+
file: this.file,
|
|
944
|
+
start: startToken.span.start,
|
|
945
|
+
end: rbrace.span.end,
|
|
946
|
+
},
|
|
947
|
+
};
|
|
948
|
+
|
|
949
|
+
// Declare the relationship in current scope (both identifiers)
|
|
950
|
+
this.declareRelationship(relationshipDecl);
|
|
951
|
+
|
|
952
|
+
return relationshipDecl;
|
|
953
|
+
}
|
|
954
|
+
|
|
472
955
|
/**
|
|
473
956
|
* @returns {FromBlock}
|
|
474
957
|
*/
|
|
@@ -492,11 +975,54 @@ class Parser {
|
|
|
492
975
|
}
|
|
493
976
|
|
|
494
977
|
/**
|
|
978
|
+
* Parses a relationships block
|
|
979
|
+
* Supports both legacy syntax (string literals for relates blocks) and new syntax (relationship IDs + targets)
|
|
495
980
|
* @returns {RelationshipsBlock}
|
|
496
981
|
*/
|
|
497
982
|
parseRelationships() {
|
|
498
983
|
const startToken = this.expect('KEYWORD', 'relationships');
|
|
499
984
|
const lbrace = this.expect('LBRACE');
|
|
985
|
+
const startOffset = lbrace.span.end.offset;
|
|
986
|
+
|
|
987
|
+
// Try to detect which syntax we're using
|
|
988
|
+
// If first token after brace is a STRING, use legacy syntax
|
|
989
|
+
// Otherwise, use new syntax (relationship ID + targets)
|
|
990
|
+
|
|
991
|
+
// Peek ahead to see what comes next
|
|
992
|
+
let peekPos = this.pos;
|
|
993
|
+
let foundString = false;
|
|
994
|
+
let foundIdentifier = false;
|
|
995
|
+
|
|
996
|
+
while (peekPos < this.tokens.length && peekPos < this.pos + 10) {
|
|
997
|
+
const token = this.tokens[peekPos];
|
|
998
|
+
if (token.type === 'RBRACE' || token.type === 'EOF') break;
|
|
999
|
+
if (token.type === 'STRING') {
|
|
1000
|
+
foundString = true;
|
|
1001
|
+
break;
|
|
1002
|
+
}
|
|
1003
|
+
if (token.type === 'IDENTIFIER' || token.type === 'KEYWORD') {
|
|
1004
|
+
foundIdentifier = true;
|
|
1005
|
+
break;
|
|
1006
|
+
}
|
|
1007
|
+
peekPos++;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
// If we found a string first, use legacy syntax (for relates blocks)
|
|
1011
|
+
if (foundString && !foundIdentifier) {
|
|
1012
|
+
return this.parseRelationshipsLegacy(startToken, lbrace);
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
// Otherwise, use new syntax (relationship ID + targets)
|
|
1016
|
+
return this.parseRelationshipsNew(startToken, lbrace);
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
/**
|
|
1020
|
+
* Parses legacy relationships block (string literals for relates blocks)
|
|
1021
|
+
* @param {Token} startToken
|
|
1022
|
+
* @param {Token} lbrace
|
|
1023
|
+
* @returns {RelationshipsBlock}
|
|
1024
|
+
*/
|
|
1025
|
+
parseRelationshipsLegacy(startToken, lbrace) {
|
|
500
1026
|
const values = [];
|
|
501
1027
|
let relationshipsSource = {
|
|
502
1028
|
file: this.file,
|
|
@@ -530,6 +1056,89 @@ class Parser {
|
|
|
530
1056
|
};
|
|
531
1057
|
}
|
|
532
1058
|
|
|
1059
|
+
/**
|
|
1060
|
+
* Parses new relationships block syntax (relationship ID + target identifiers)
|
|
1061
|
+
* @param {Token} startToken
|
|
1062
|
+
* @param {Token} lbrace
|
|
1063
|
+
* @returns {RelationshipsBlock}
|
|
1064
|
+
*/
|
|
1065
|
+
parseRelationshipsNew(startToken, lbrace) {
|
|
1066
|
+
const entries = [];
|
|
1067
|
+
let relationshipsSource = {
|
|
1068
|
+
file: this.file,
|
|
1069
|
+
start: lbrace.span.end,
|
|
1070
|
+
end: lbrace.span.end,
|
|
1071
|
+
};
|
|
1072
|
+
|
|
1073
|
+
while (!this.match('RBRACE') && !this.match('EOF')) {
|
|
1074
|
+
// Parse relationship ID (identifier or keyword)
|
|
1075
|
+
if (this.match('IDENTIFIER') || this.match('KEYWORD')) {
|
|
1076
|
+
const relationshipIdToken = this.advance();
|
|
1077
|
+
const relationshipId = relationshipIdToken.value;
|
|
1078
|
+
|
|
1079
|
+
// Expect opening brace for targets
|
|
1080
|
+
if (!this.match('LBRACE')) {
|
|
1081
|
+
throw new Error(
|
|
1082
|
+
`Expected '{' after relationship identifier "${relationshipId}" at ${this.file}:${relationshipIdToken.span.start.line}`,
|
|
1083
|
+
);
|
|
1084
|
+
}
|
|
1085
|
+
const targetLbrace = this.advance();
|
|
1086
|
+
|
|
1087
|
+
const targets = [];
|
|
1088
|
+
|
|
1089
|
+
// Parse targets until closing brace
|
|
1090
|
+
while (!this.match('RBRACE') && !this.match('EOF')) {
|
|
1091
|
+
if (this.match('IDENTIFIER') || this.match('KEYWORD')) {
|
|
1092
|
+
const targetIdToken = this.advance();
|
|
1093
|
+
const targetId = targetIdToken.value;
|
|
1094
|
+
|
|
1095
|
+
// Check if this target has a metadata block
|
|
1096
|
+
let metadata = null;
|
|
1097
|
+
if (this.match('LBRACE')) {
|
|
1098
|
+
// Parse metadata block (describe)
|
|
1099
|
+
const metadataLbrace = this.advance();
|
|
1100
|
+
const metadataBody = this.parseBlockBody(['describe'], {});
|
|
1101
|
+
const metadataRbrace = this.expect('RBRACE');
|
|
1102
|
+
|
|
1103
|
+
const describeBlock = metadataBody.find((b) => b.kind === 'describe');
|
|
1104
|
+
if (describeBlock) {
|
|
1105
|
+
metadata = describeBlock;
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
targets.push({
|
|
1110
|
+
id: targetId,
|
|
1111
|
+
metadata: metadata || undefined,
|
|
1112
|
+
});
|
|
1113
|
+
} else {
|
|
1114
|
+
// Skip other tokens
|
|
1115
|
+
this.advance();
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
const targetRbrace = this.expect('RBRACE');
|
|
1120
|
+
relationshipsSource.end = targetRbrace.span.end;
|
|
1121
|
+
|
|
1122
|
+
entries.push({
|
|
1123
|
+
relationshipId,
|
|
1124
|
+
targets,
|
|
1125
|
+
});
|
|
1126
|
+
} else {
|
|
1127
|
+
// Skip other tokens
|
|
1128
|
+
this.advance();
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
const rbrace = this.expect('RBRACE');
|
|
1133
|
+
relationshipsSource.end = rbrace.span.start;
|
|
1134
|
+
|
|
1135
|
+
return {
|
|
1136
|
+
kind: 'relationships',
|
|
1137
|
+
entries,
|
|
1138
|
+
source: relationshipsSource,
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
|
|
533
1142
|
/**
|
|
534
1143
|
* Parses a describe block, consuming raw text until matching closing brace
|
|
535
1144
|
* Treats braces inside as plain text (doesn't parse nested structures)
|
|
@@ -775,8 +1384,8 @@ class Parser {
|
|
|
775
1384
|
* Parses a reference declaration: reference <Name> [in <RepoName>] { ... }
|
|
776
1385
|
* @returns {ReferenceDecl}
|
|
777
1386
|
*/
|
|
778
|
-
parseReferenceDecl() {
|
|
779
|
-
const startToken = this.
|
|
1387
|
+
parseReferenceDecl(spelledKind = 'reference') {
|
|
1388
|
+
const startToken = this.expectKindToken(spelledKind);
|
|
780
1389
|
let nameToken = null;
|
|
781
1390
|
if (this.match('IDENTIFIER')) {
|
|
782
1391
|
nameToken = this.expect('IDENTIFIER');
|
|
@@ -1000,8 +1609,8 @@ class Parser {
|
|
|
1000
1609
|
* Parses a repository block: repository <Identifier> { url { ... } ... }
|
|
1001
1610
|
* @returns {RepositoryDecl}
|
|
1002
1611
|
*/
|
|
1003
|
-
parseRepository() {
|
|
1004
|
-
const startToken = this.
|
|
1612
|
+
parseRepository(spelledKind = 'repository') {
|
|
1613
|
+
const startToken = this.expectKindToken(spelledKind);
|
|
1005
1614
|
const nameToken = this.expect('IDENTIFIER');
|
|
1006
1615
|
let parentName = undefined;
|
|
1007
1616
|
|