@sprig-and-prose/sprig-universe 0.2.0 → 0.3.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/package.json +1 -1
- package/src/ast.js +30 -32
- package/src/cli.js +1 -210
- package/src/graph.js +635 -147
- package/src/ir.js +21 -6
- package/src/parser.js +225 -215
- package/test/fixtures/amaranthine-mini.prose +14 -8
- package/test/fixtures/multi-file-universe-a.prose +9 -4
- package/test/fixtures/multi-file-universe-b.prose +5 -4
- package/test/fixtures/named-duplicate.prose +6 -4
- package/test/fixtures/reference-attachments.prose +19 -0
- package/test/fixtures/reference-commas.prose +15 -0
- package/test/fixtures/reference-inline.prose +14 -0
- package/test/fixtures/reference-raw-url.prose +9 -0
- package/test/fixtures/reference-repo-paths.prose +11 -0
- package/test/fixtures/reference-unknown.prose +7 -0
- package/test/references.test.js +105 -0
- package/test/universe-basic.test.js +21 -166
- package/repositories/sprig-repository-github/index.js +0 -29
package/src/parser.js
CHANGED
|
@@ -16,9 +16,8 @@ import { mergeSpans } from './util/span.js';
|
|
|
16
16
|
* @typedef {import('./ast.js').DescribeBlock} DescribeBlock
|
|
17
17
|
* @typedef {import('./ast.js').UnknownBlock} UnknownBlock
|
|
18
18
|
* @typedef {import('./ast.js').ReferencesBlock} ReferencesBlock
|
|
19
|
-
* @typedef {import('./ast.js').
|
|
20
|
-
* @typedef {import('./ast.js').
|
|
21
|
-
* @typedef {import('./ast.js').UsingInReferencesBlock} UsingInReferencesBlock
|
|
19
|
+
* @typedef {import('./ast.js').RepositoryDecl} RepositoryDecl
|
|
20
|
+
* @typedef {import('./ast.js').ReferenceDecl} ReferenceDecl
|
|
22
21
|
* @typedef {import('./ast.js').DocumentationBlock} DocumentationBlock
|
|
23
22
|
* @typedef {import('./ast.js').DocumentBlock} DocumentBlock
|
|
24
23
|
* @typedef {import('./ast.js').NamedDocumentBlock} NamedDocumentBlock
|
|
@@ -106,7 +105,6 @@ class Parser {
|
|
|
106
105
|
parseFile() {
|
|
107
106
|
const universes = [];
|
|
108
107
|
const scenes = [];
|
|
109
|
-
const topLevelDeclsByUniverse = new Map();
|
|
110
108
|
const topLevelDeclsUnscoped = [];
|
|
111
109
|
const startToken = this.peek();
|
|
112
110
|
|
|
@@ -128,18 +126,14 @@ class Parser {
|
|
|
128
126
|
decl = this.parseConcept();
|
|
129
127
|
} else if (keyword === 'anthology') {
|
|
130
128
|
decl = this.parseAnthology();
|
|
129
|
+
} else if (keyword === 'repository') {
|
|
130
|
+
decl = this.parseRepository();
|
|
131
|
+
} else if (keyword === 'reference') {
|
|
132
|
+
decl = this.parseReferenceDecl();
|
|
131
133
|
}
|
|
132
134
|
|
|
133
135
|
if (decl) {
|
|
134
|
-
|
|
135
|
-
const universeName = decl.parentName;
|
|
136
|
-
if (!topLevelDeclsByUniverse.has(universeName)) {
|
|
137
|
-
topLevelDeclsByUniverse.set(universeName, []);
|
|
138
|
-
}
|
|
139
|
-
topLevelDeclsByUniverse.get(universeName).push(decl);
|
|
140
|
-
} else {
|
|
141
|
-
topLevelDeclsUnscoped.push(decl);
|
|
142
|
-
}
|
|
136
|
+
topLevelDeclsUnscoped.push(decl);
|
|
143
137
|
} else {
|
|
144
138
|
// Skip unknown top-level content (tolerant parsing)
|
|
145
139
|
const token = this.advance();
|
|
@@ -156,43 +150,16 @@ class Parser {
|
|
|
156
150
|
}
|
|
157
151
|
}
|
|
158
152
|
|
|
159
|
-
// Merge top-level declarations into their target
|
|
160
|
-
if (
|
|
161
|
-
|
|
162
|
-
for (const universe of universes) {
|
|
163
|
-
universesByName.set(universe.name, universe);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
for (const [universeName, decls] of topLevelDeclsByUniverse.entries()) {
|
|
167
|
-
const universe = universesByName.get(universeName);
|
|
168
|
-
if (universe) {
|
|
169
|
-
universe.body.push(...decls);
|
|
170
|
-
} else {
|
|
171
|
-
const firstDecl = decls[0];
|
|
172
|
-
const lastDecl = decls[decls.length - 1];
|
|
173
|
-
universes.push({
|
|
174
|
-
kind: 'universe',
|
|
175
|
-
name: universeName,
|
|
176
|
-
body: decls,
|
|
177
|
-
source: {
|
|
178
|
-
file: this.file,
|
|
179
|
-
start: firstDecl.source.start,
|
|
180
|
-
end: lastDecl.source.end,
|
|
181
|
-
},
|
|
182
|
-
});
|
|
183
|
-
universesByName.set(universeName, universes[universes.length - 1]);
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
if (topLevelDeclsUnscoped.length > 0 && universes.length === 1) {
|
|
188
|
-
universes[0].body.push(...topLevelDeclsUnscoped);
|
|
189
|
-
}
|
|
153
|
+
// Merge top-level declarations into their target universe
|
|
154
|
+
if (topLevelDeclsUnscoped.length > 0 && universes.length === 1) {
|
|
155
|
+
universes[0].body.push(...topLevelDeclsUnscoped);
|
|
190
156
|
}
|
|
191
157
|
|
|
192
158
|
return {
|
|
193
159
|
file: this.file,
|
|
194
160
|
universes,
|
|
195
161
|
scenes,
|
|
162
|
+
topLevelDecls: topLevelDeclsUnscoped.length > 0 ? topLevelDeclsUnscoped : undefined,
|
|
196
163
|
source: startToken
|
|
197
164
|
? {
|
|
198
165
|
file: this.file,
|
|
@@ -210,7 +177,7 @@ class Parser {
|
|
|
210
177
|
const startToken = this.expect('KEYWORD', 'universe');
|
|
211
178
|
const nameToken = this.expect('IDENTIFIER');
|
|
212
179
|
const lbrace = this.expect('LBRACE');
|
|
213
|
-
const body = this.parseBlockBody(['anthology', 'series', 'book', 'chapter', 'concept', 'relates', 'describe', 'title', 'repository']);
|
|
180
|
+
const body = this.parseBlockBody(['anthology', 'series', 'book', 'chapter', 'concept', 'relates', 'describe', 'title', 'repository', 'reference', 'references', 'documentation']);
|
|
214
181
|
const rbrace = this.expect('RBRACE');
|
|
215
182
|
|
|
216
183
|
return {
|
|
@@ -227,7 +194,7 @@ class Parser {
|
|
|
227
194
|
|
|
228
195
|
/**
|
|
229
196
|
* @param {string[]} allowedKeywords - Keywords allowed in this body
|
|
230
|
-
* @returns {Array<SeriesDecl | BookDecl | ChapterDecl | ConceptDecl | RelatesDecl | DescribeBlock | TitleBlock | FromBlock | RelationshipsBlock | ReferencesBlock | DocumentationBlock |
|
|
197
|
+
* @returns {Array<SeriesDecl | BookDecl | ChapterDecl | ConceptDecl | RelatesDecl | DescribeBlock | TitleBlock | FromBlock | RelationshipsBlock | ReferencesBlock | DocumentationBlock | RepositoryDecl | ReferenceDecl | NamedDocumentBlock | UnknownBlock>}
|
|
231
198
|
*/
|
|
232
199
|
parseBlockBody(allowedKeywords) {
|
|
233
200
|
const body = [];
|
|
@@ -238,21 +205,6 @@ class Parser {
|
|
|
238
205
|
const keyword = this.peek()?.value;
|
|
239
206
|
if (!keyword) break;
|
|
240
207
|
|
|
241
|
-
// Check for named reference: reference <IDENTIFIER> { ... }
|
|
242
|
-
if (keyword === 'reference') {
|
|
243
|
-
const nextPos = this.pos + 1;
|
|
244
|
-
if (nextPos < this.tokens.length &&
|
|
245
|
-
this.tokens[nextPos].type === 'IDENTIFIER' &&
|
|
246
|
-
nextPos + 1 < this.tokens.length &&
|
|
247
|
-
this.tokens[nextPos + 1].type === 'LBRACE') {
|
|
248
|
-
// This is a named reference block
|
|
249
|
-
body.push(this.parseNamedReference());
|
|
250
|
-
continue;
|
|
251
|
-
}
|
|
252
|
-
// Otherwise, it's an inline reference (only valid inside references block)
|
|
253
|
-
// Fall through to unknown block handling
|
|
254
|
-
}
|
|
255
|
-
|
|
256
208
|
// Check for named document: document <IDENTIFIER> { ... }
|
|
257
209
|
if (keyword === 'document') {
|
|
258
210
|
const nextPos = this.pos + 1;
|
|
@@ -286,13 +238,15 @@ class Parser {
|
|
|
286
238
|
body.push(this.parseTitle());
|
|
287
239
|
} else if (keyword === 'repository' && allowedKeywords.includes('repository')) {
|
|
288
240
|
body.push(this.parseRepository());
|
|
241
|
+
} else if (keyword === 'reference' && allowedKeywords.includes('reference')) {
|
|
242
|
+
body.push(this.parseReferenceDecl());
|
|
289
243
|
} else if (keyword === 'from' && allowedKeywords.includes('from')) {
|
|
290
244
|
body.push(this.parseFrom());
|
|
291
245
|
} else if (keyword === 'relationships' && allowedKeywords.includes('relationships')) {
|
|
292
246
|
body.push(this.parseRelationships());
|
|
293
|
-
} else if (keyword === 'references') {
|
|
247
|
+
} else if (keyword === 'references' && allowedKeywords.includes('references')) {
|
|
294
248
|
body.push(this.parseReferences());
|
|
295
|
-
} else if (keyword === 'documentation') {
|
|
249
|
+
} else if (keyword === 'documentation' && allowedKeywords.includes('documentation')) {
|
|
296
250
|
body.push(this.parseDocumentation());
|
|
297
251
|
} else {
|
|
298
252
|
// Unknown keyword/identifier followed by brace - parse as UnknownBlock
|
|
@@ -320,13 +274,32 @@ class Parser {
|
|
|
320
274
|
parseAnthology() {
|
|
321
275
|
const startToken = this.expect('KEYWORD', 'anthology');
|
|
322
276
|
const nameToken = this.expect('IDENTIFIER');
|
|
277
|
+
let parentName = undefined;
|
|
278
|
+
if (this.match('KEYWORD') && this.peek()?.value === 'in') {
|
|
279
|
+
this.expect('KEYWORD', 'in');
|
|
280
|
+
const parentToken = this.expect('IDENTIFIER');
|
|
281
|
+
parentName = parentToken.value;
|
|
282
|
+
}
|
|
323
283
|
const lbrace = this.expect('LBRACE');
|
|
324
|
-
const body = this.parseBlockBody([
|
|
284
|
+
const body = this.parseBlockBody([
|
|
285
|
+
'describe',
|
|
286
|
+
'title',
|
|
287
|
+
'series',
|
|
288
|
+
'book',
|
|
289
|
+
'chapter',
|
|
290
|
+
'concept',
|
|
291
|
+
'relates',
|
|
292
|
+
'references',
|
|
293
|
+
'documentation',
|
|
294
|
+
'repository',
|
|
295
|
+
'reference',
|
|
296
|
+
]);
|
|
325
297
|
const rbrace = this.expect('RBRACE');
|
|
326
298
|
|
|
327
299
|
return {
|
|
328
300
|
kind: 'anthology',
|
|
329
301
|
name: nameToken.value,
|
|
302
|
+
parentName,
|
|
330
303
|
body,
|
|
331
304
|
source: {
|
|
332
305
|
file: this.file,
|
|
@@ -352,7 +325,7 @@ class Parser {
|
|
|
352
325
|
}
|
|
353
326
|
|
|
354
327
|
const lbrace = this.expect('LBRACE');
|
|
355
|
-
const body = this.parseBlockBody(['book', 'chapter', 'describe', 'title', 'references', 'documentation']);
|
|
328
|
+
const body = this.parseBlockBody(['book', 'chapter', 'describe', 'title', 'references', 'documentation', 'repository', 'reference']);
|
|
356
329
|
const rbrace = this.expect('RBRACE');
|
|
357
330
|
|
|
358
331
|
return {
|
|
@@ -377,7 +350,7 @@ class Parser {
|
|
|
377
350
|
this.expect('KEYWORD', 'in');
|
|
378
351
|
const parentToken = this.expect('IDENTIFIER');
|
|
379
352
|
const lbrace = this.expect('LBRACE');
|
|
380
|
-
const body = this.parseBlockBody(['chapter', 'describe', 'title', 'references', 'documentation']);
|
|
353
|
+
const body = this.parseBlockBody(['chapter', 'describe', 'title', 'references', 'documentation', 'repository', 'reference']);
|
|
381
354
|
const rbrace = this.expect('RBRACE');
|
|
382
355
|
|
|
383
356
|
return {
|
|
@@ -402,7 +375,7 @@ class Parser {
|
|
|
402
375
|
this.expect('KEYWORD', 'in');
|
|
403
376
|
const parentToken = this.expect('IDENTIFIER');
|
|
404
377
|
const lbrace = this.expect('LBRACE');
|
|
405
|
-
const body = this.parseBlockBody(['describe', 'title', 'references', 'documentation']);
|
|
378
|
+
const body = this.parseBlockBody(['describe', 'title', 'references', 'documentation', 'repository', 'reference']);
|
|
406
379
|
const rbrace = this.expect('RBRACE');
|
|
407
380
|
|
|
408
381
|
return {
|
|
@@ -434,7 +407,7 @@ class Parser {
|
|
|
434
407
|
}
|
|
435
408
|
|
|
436
409
|
const lbrace = this.expect('LBRACE');
|
|
437
|
-
const body = this.parseBlockBody(['describe', 'title', 'references', 'documentation']);
|
|
410
|
+
const body = this.parseBlockBody(['describe', 'title', 'references', 'documentation', 'repository', 'reference']);
|
|
438
411
|
const rbrace = this.expect('RBRACE');
|
|
439
412
|
|
|
440
413
|
return {
|
|
@@ -652,193 +625,141 @@ class Parser {
|
|
|
652
625
|
}
|
|
653
626
|
|
|
654
627
|
/**
|
|
655
|
-
* Parses a references block containing
|
|
628
|
+
* Parses a references block containing a list of identifier paths
|
|
656
629
|
* @returns {ReferencesBlock}
|
|
657
630
|
*/
|
|
658
631
|
parseReferences() {
|
|
659
632
|
const startToken = this.expect('IDENTIFIER', 'references');
|
|
660
633
|
const lbrace = this.expect('LBRACE');
|
|
661
|
-
const
|
|
634
|
+
const items = [];
|
|
635
|
+
let depth = 1;
|
|
662
636
|
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
// Look for 'reference' identifier
|
|
670
|
-
references.push(this.parseReference());
|
|
671
|
-
} else {
|
|
672
|
-
// Skip unexpected tokens (tolerant parsing)
|
|
637
|
+
while (this.pos < this.tokens.length && !this.match('EOF')) {
|
|
638
|
+
if (this.match('LBRACE')) {
|
|
639
|
+
throw new Error(`Unexpected '{' in references block at ${this.file}:${this.peek()?.span.start.line}. Use identifiers only.`);
|
|
640
|
+
}
|
|
641
|
+
if (this.match('LBRACE')) {
|
|
642
|
+
depth += 1;
|
|
673
643
|
this.advance();
|
|
644
|
+
continue;
|
|
674
645
|
}
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
const rbrace = this.expect('RBRACE');
|
|
678
|
-
|
|
679
|
-
return {
|
|
680
|
-
kind: 'references',
|
|
681
|
-
references,
|
|
682
|
-
source: {
|
|
683
|
-
file: this.file,
|
|
684
|
-
start: startToken.span.start,
|
|
685
|
-
end: rbrace.span.end,
|
|
686
|
-
},
|
|
687
|
-
};
|
|
688
|
-
}
|
|
689
646
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
* @returns {UsingInReferencesBlock}
|
|
693
|
-
*/
|
|
694
|
-
parseUsingInReferences() {
|
|
695
|
-
const startToken = this.expect('KEYWORD', 'using');
|
|
696
|
-
const lbrace = this.expect('LBRACE');
|
|
697
|
-
const names = [];
|
|
698
|
-
|
|
699
|
-
while (!this.match('RBRACE') && !this.match('EOF')) {
|
|
700
|
-
if (this.match('IDENTIFIER')) {
|
|
701
|
-
names.push(this.expect('IDENTIFIER').value);
|
|
702
|
-
// Skip optional comma
|
|
703
|
-
if (this.match('COMMA')) {
|
|
704
|
-
this.expect('COMMA');
|
|
705
|
-
}
|
|
706
|
-
} else if (this.match('COMMA')) {
|
|
707
|
-
// Skip stray commas
|
|
708
|
-
this.expect('COMMA');
|
|
709
|
-
} else {
|
|
710
|
-
// Skip non-identifiers (tolerant parsing)
|
|
647
|
+
if (this.match('RBRACE')) {
|
|
648
|
+
depth -= 1;
|
|
711
649
|
this.advance();
|
|
650
|
+
if (depth === 0) {
|
|
651
|
+
break;
|
|
652
|
+
}
|
|
653
|
+
continue;
|
|
712
654
|
}
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
const rbrace = this.expect('RBRACE');
|
|
716
|
-
|
|
717
|
-
return {
|
|
718
|
-
kind: 'using-in-references',
|
|
719
|
-
names,
|
|
720
|
-
source: {
|
|
721
|
-
file: this.file,
|
|
722
|
-
start: startToken.span.start,
|
|
723
|
-
end: rbrace.span.end,
|
|
724
|
-
},
|
|
725
|
-
};
|
|
726
|
-
}
|
|
727
655
|
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
parseReference() {
|
|
733
|
-
const startToken = this.expect('IDENTIFIER', 'reference');
|
|
734
|
-
const lbrace = this.expect('LBRACE');
|
|
735
|
-
let repository = null;
|
|
736
|
-
let paths = [];
|
|
737
|
-
let describe = null;
|
|
738
|
-
let kind = null;
|
|
656
|
+
if (depth !== 1) {
|
|
657
|
+
this.advance();
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
739
660
|
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
661
|
+
if (this.match('COMMA')) {
|
|
662
|
+
this.advance();
|
|
663
|
+
continue;
|
|
664
|
+
}
|
|
743
665
|
|
|
744
|
-
// Check for identifier or keyword (describe is a keyword)
|
|
745
666
|
if (this.match('IDENTIFIER') || this.match('KEYWORD')) {
|
|
746
|
-
const
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
667
|
+
const startToken = this.peek();
|
|
668
|
+
const name = this.parseIdentifierPath();
|
|
669
|
+
if (name && startToken) {
|
|
670
|
+
const lastToken = this.tokens[this.pos - 1] || startToken;
|
|
671
|
+
items.push({
|
|
672
|
+
name,
|
|
673
|
+
source: {
|
|
674
|
+
file: this.file,
|
|
675
|
+
start: startToken.span.start,
|
|
676
|
+
end: lastToken.span.end,
|
|
677
|
+
},
|
|
678
|
+
});
|
|
758
679
|
}
|
|
759
|
-
|
|
760
|
-
// Skip unexpected tokens
|
|
761
|
-
this.advance();
|
|
680
|
+
continue;
|
|
762
681
|
}
|
|
763
|
-
}
|
|
764
682
|
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
if (!repository) {
|
|
768
|
-
throw new Error(`Missing 'repository' field in reference block at ${this.file}:${startToken.span.start.line}`);
|
|
769
|
-
}
|
|
770
|
-
if (paths.length === 0) {
|
|
771
|
-
throw new Error(`Missing or empty 'paths' field in reference block at ${this.file}:${startToken.span.start.line}`);
|
|
683
|
+
this.advance();
|
|
772
684
|
}
|
|
773
685
|
|
|
774
686
|
return {
|
|
775
|
-
kind: '
|
|
776
|
-
|
|
777
|
-
paths,
|
|
778
|
-
referenceKind: kind,
|
|
779
|
-
describe,
|
|
687
|
+
kind: 'references',
|
|
688
|
+
items,
|
|
780
689
|
source: {
|
|
781
690
|
file: this.file,
|
|
782
691
|
start: startToken.span.start,
|
|
783
|
-
end:
|
|
692
|
+
end: this.tokens[this.pos - 1]?.span.end || lbrace.span.end,
|
|
784
693
|
},
|
|
785
694
|
};
|
|
786
695
|
}
|
|
787
696
|
|
|
788
697
|
/**
|
|
789
|
-
* Parses a
|
|
790
|
-
* @returns {
|
|
698
|
+
* Parses a reference declaration: reference <Name> [in <RepoName>] { ... }
|
|
699
|
+
* @returns {ReferenceDecl}
|
|
791
700
|
*/
|
|
792
|
-
|
|
793
|
-
const startToken = this.expect('IDENTIFIER', 'reference');
|
|
794
|
-
|
|
701
|
+
parseReferenceDecl() {
|
|
702
|
+
const startToken = this.match('KEYWORD') ? this.expect('KEYWORD', 'reference') : this.expect('IDENTIFIER', 'reference');
|
|
703
|
+
let nameToken = null;
|
|
704
|
+
if (this.match('IDENTIFIER')) {
|
|
705
|
+
nameToken = this.expect('IDENTIFIER');
|
|
706
|
+
}
|
|
707
|
+
let repositoryName = null;
|
|
708
|
+
|
|
709
|
+
if (this.match('KEYWORD') && this.peek()?.value === 'in') {
|
|
710
|
+
this.expect('KEYWORD', 'in');
|
|
711
|
+
const repoName = this.parseIdentifierPath();
|
|
712
|
+
if (!repoName) {
|
|
713
|
+
throw new Error(`Expected repository name after "in" at ${this.file}:${this.peek()?.span.start.line || '?'}`);
|
|
714
|
+
}
|
|
715
|
+
repositoryName = repoName;
|
|
716
|
+
}
|
|
717
|
+
|
|
795
718
|
const lbrace = this.expect('LBRACE');
|
|
796
|
-
let
|
|
797
|
-
let paths =
|
|
798
|
-
let describe = null;
|
|
719
|
+
let url = null;
|
|
720
|
+
let paths = null;
|
|
799
721
|
let kind = null;
|
|
722
|
+
let describe = null;
|
|
723
|
+
let note = null;
|
|
724
|
+
let title = null;
|
|
800
725
|
|
|
801
|
-
// Parse repository, paths, optional kind, and optional describe (same as parseReference)
|
|
802
726
|
while (!this.match('RBRACE') && !this.match('EOF')) {
|
|
803
727
|
if (this.match('RBRACE')) break;
|
|
804
728
|
|
|
805
|
-
// Check for identifier or keyword (describe is a keyword)
|
|
806
729
|
if (this.match('IDENTIFIER') || this.match('KEYWORD')) {
|
|
807
730
|
const identifier = this.peek()?.value;
|
|
808
|
-
if (identifier === '
|
|
809
|
-
|
|
731
|
+
if (identifier === 'url') {
|
|
732
|
+
url = this.parseStringBlock('url');
|
|
810
733
|
} else if (identifier === 'paths') {
|
|
811
734
|
paths = this.parsePathsBlock();
|
|
812
735
|
} else if (identifier === 'kind') {
|
|
813
736
|
kind = this.parseStringBlock('kind');
|
|
814
737
|
} else if (identifier === 'describe') {
|
|
815
738
|
describe = this.parseDescribe();
|
|
739
|
+
} else if (identifier === 'note') {
|
|
740
|
+
note = this.parseNote();
|
|
741
|
+
} else if (identifier === 'title') {
|
|
742
|
+
title = this.parseTitle();
|
|
816
743
|
} else {
|
|
817
|
-
// Skip unknown identifier/keyword
|
|
818
744
|
this.advance();
|
|
819
745
|
}
|
|
820
746
|
} else {
|
|
821
|
-
// Skip unexpected tokens
|
|
822
747
|
this.advance();
|
|
823
748
|
}
|
|
824
749
|
}
|
|
825
750
|
|
|
826
751
|
const rbrace = this.expect('RBRACE');
|
|
827
752
|
|
|
828
|
-
if (!repository) {
|
|
829
|
-
throw new Error(`Missing 'repository' field in named reference block at ${this.file}:${startToken.span.start.line}`);
|
|
830
|
-
}
|
|
831
|
-
if (paths.length === 0) {
|
|
832
|
-
throw new Error(`Missing or empty 'paths' field in named reference block at ${this.file}:${startToken.span.start.line}`);
|
|
833
|
-
}
|
|
834
|
-
|
|
835
753
|
return {
|
|
836
|
-
kind: '
|
|
837
|
-
name: nameToken.value,
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
754
|
+
kind: 'reference',
|
|
755
|
+
name: nameToken ? nameToken.value : undefined,
|
|
756
|
+
repositoryName: repositoryName || undefined,
|
|
757
|
+
url: url || undefined,
|
|
758
|
+
paths: paths || undefined,
|
|
759
|
+
referenceKind: kind || undefined,
|
|
760
|
+
describe: describe || undefined,
|
|
761
|
+
note: note || undefined,
|
|
762
|
+
title: title || undefined,
|
|
842
763
|
source: {
|
|
843
764
|
file: this.file,
|
|
844
765
|
start: startToken.span.start,
|
|
@@ -999,45 +920,58 @@ class Parser {
|
|
|
999
920
|
}
|
|
1000
921
|
|
|
1001
922
|
/**
|
|
1002
|
-
* Parses a repository block: repository <Identifier> {
|
|
923
|
+
* Parses a repository block: repository <Identifier> { url { ... } ... }
|
|
1003
924
|
* @returns {RepositoryDecl}
|
|
1004
925
|
*/
|
|
1005
926
|
parseRepository() {
|
|
1006
927
|
const startToken = this.expect('KEYWORD', 'repository');
|
|
1007
928
|
const nameToken = this.expect('IDENTIFIER');
|
|
929
|
+
let parentName = undefined;
|
|
930
|
+
|
|
931
|
+
if (this.match('KEYWORD') && this.peek()?.value === 'in') {
|
|
932
|
+
this.expect('KEYWORD', 'in');
|
|
933
|
+
const containerName = this.parseIdentifierPath();
|
|
934
|
+
if (!containerName) {
|
|
935
|
+
throw new Error(`Expected container name after "in" at ${this.file}:${this.peek()?.span.start.line || '?'}`);
|
|
936
|
+
}
|
|
937
|
+
parentName = containerName;
|
|
938
|
+
}
|
|
1008
939
|
const lbrace = this.expect('LBRACE');
|
|
1009
940
|
|
|
1010
|
-
let
|
|
1011
|
-
let
|
|
941
|
+
let url = null;
|
|
942
|
+
let describe = null;
|
|
943
|
+
let note = null;
|
|
944
|
+
let title = null;
|
|
1012
945
|
|
|
1013
946
|
while (!this.match('RBRACE') && !this.match('EOF')) {
|
|
1014
947
|
if (this.match('IDENTIFIER') || this.match('KEYWORD')) {
|
|
1015
948
|
const identifier = this.peek()?.value;
|
|
1016
|
-
if (identifier === '
|
|
1017
|
-
|
|
1018
|
-
} else if (identifier === '
|
|
1019
|
-
|
|
949
|
+
if (identifier === 'url') {
|
|
950
|
+
url = this.parseStringBlock('url');
|
|
951
|
+
} else if (identifier === 'describe') {
|
|
952
|
+
describe = this.parseDescribe();
|
|
953
|
+
} else if (identifier === 'note') {
|
|
954
|
+
note = this.parseNote();
|
|
955
|
+
} else if (identifier === 'title') {
|
|
956
|
+
title = this.parseTitle();
|
|
1020
957
|
} else {
|
|
1021
|
-
// Skip unknown identifier/keyword
|
|
1022
958
|
this.advance();
|
|
1023
959
|
}
|
|
1024
960
|
} else {
|
|
1025
|
-
// Skip unexpected tokens
|
|
1026
961
|
this.advance();
|
|
1027
962
|
}
|
|
1028
963
|
}
|
|
1029
964
|
|
|
1030
965
|
const rbrace = this.expect('RBRACE');
|
|
1031
966
|
|
|
1032
|
-
if (!kind) {
|
|
1033
|
-
throw new Error(`Missing 'kind' field in repository block at ${this.file}:${startToken.span.start.line}`);
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
967
|
return {
|
|
1037
968
|
kind: 'repository',
|
|
1038
969
|
name: nameToken.value,
|
|
1039
|
-
|
|
1040
|
-
|
|
970
|
+
parentName,
|
|
971
|
+
url: url || undefined,
|
|
972
|
+
describe: describe || undefined,
|
|
973
|
+
note: note || undefined,
|
|
974
|
+
title: title || undefined,
|
|
1041
975
|
source: {
|
|
1042
976
|
file: this.file,
|
|
1043
977
|
start: startToken.span.start,
|
|
@@ -1046,6 +980,82 @@ class Parser {
|
|
|
1046
980
|
};
|
|
1047
981
|
}
|
|
1048
982
|
|
|
983
|
+
/**
|
|
984
|
+
* Parses an identifier path (IDENTIFIER (DOT IDENTIFIER)*)
|
|
985
|
+
* @returns {string | null}
|
|
986
|
+
*/
|
|
987
|
+
parseIdentifierPath() {
|
|
988
|
+
if (!this.match('IDENTIFIER') && !this.match('KEYWORD')) {
|
|
989
|
+
return null;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
const parts = [];
|
|
993
|
+
parts.push(this.advance().value);
|
|
994
|
+
|
|
995
|
+
while (this.match('DOT')) {
|
|
996
|
+
this.expect('DOT');
|
|
997
|
+
if (this.match('IDENTIFIER') || this.match('KEYWORD')) {
|
|
998
|
+
parts.push(this.advance().value);
|
|
999
|
+
} else {
|
|
1000
|
+
break;
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
return parts.join('.');
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
/**
|
|
1008
|
+
* Parses a note block, same format as describe
|
|
1009
|
+
* @returns {DescribeBlock}
|
|
1010
|
+
*/
|
|
1011
|
+
parseNote() {
|
|
1012
|
+
const startToken = this.expect('IDENTIFIER', 'note');
|
|
1013
|
+
const lbrace = this.expect('LBRACE');
|
|
1014
|
+
|
|
1015
|
+
let depth = 1;
|
|
1016
|
+
const startOffset = lbrace.span.end.offset;
|
|
1017
|
+
let endOffset = startOffset;
|
|
1018
|
+
let endToken = null;
|
|
1019
|
+
|
|
1020
|
+
while (depth > 0 && this.pos < this.tokens.length) {
|
|
1021
|
+
const token = this.tokens[this.pos];
|
|
1022
|
+
if (token.type === 'EOF') break;
|
|
1023
|
+
|
|
1024
|
+
if (token.type === 'LBRACE') {
|
|
1025
|
+
depth++;
|
|
1026
|
+
this.pos++;
|
|
1027
|
+
} else if (token.type === 'RBRACE') {
|
|
1028
|
+
depth--;
|
|
1029
|
+
if (depth === 0) {
|
|
1030
|
+
endToken = token;
|
|
1031
|
+
endOffset = token.span.start.offset;
|
|
1032
|
+
this.pos++;
|
|
1033
|
+
break;
|
|
1034
|
+
} else {
|
|
1035
|
+
this.pos++;
|
|
1036
|
+
}
|
|
1037
|
+
} else {
|
|
1038
|
+
this.pos++;
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
if (depth > 0) {
|
|
1043
|
+
throw new Error(`Unclosed note block at ${this.file}:${startToken.span.start.line}`);
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
const rawContent = this.sourceText.substring(startOffset, endOffset).trim();
|
|
1047
|
+
|
|
1048
|
+
return {
|
|
1049
|
+
kind: 'note',
|
|
1050
|
+
raw: rawContent,
|
|
1051
|
+
source: {
|
|
1052
|
+
file: this.file,
|
|
1053
|
+
start: lbrace.span.end,
|
|
1054
|
+
end: endToken ? endToken.span.start : lbrace.span.end,
|
|
1055
|
+
},
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1049
1059
|
/**
|
|
1050
1060
|
* Parses a paths block (e.g., paths { 'path1', 'path2' })
|
|
1051
1061
|
* @returns {string[]}
|
|
@@ -5,6 +5,18 @@ universe Amaranthine {
|
|
|
5
5
|
progress your character.
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
repository AmaranthineRepo {
|
|
9
|
+
url { 'https://example.com/amaranthine' }
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
reference PlayerRouter in AmaranthineRepo {
|
|
13
|
+
paths { '/backends/api/src/routers/players.js' }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
reference ItemData in AmaranthineRepo {
|
|
17
|
+
paths { '/data/items/*.yaml' }
|
|
18
|
+
}
|
|
19
|
+
|
|
8
20
|
series Player {
|
|
9
21
|
describe {
|
|
10
22
|
The player is central to everything. From chat to skills to inventory,
|
|
@@ -12,10 +24,7 @@ universe Amaranthine {
|
|
|
12
24
|
}
|
|
13
25
|
|
|
14
26
|
references {
|
|
15
|
-
|
|
16
|
-
repository { 'amaranthine' }
|
|
17
|
-
paths { '/backends/api/src/routers/players.js' }
|
|
18
|
-
}
|
|
27
|
+
PlayerRouter
|
|
19
28
|
}
|
|
20
29
|
}
|
|
21
30
|
|
|
@@ -25,10 +34,7 @@ universe Amaranthine {
|
|
|
25
34
|
}
|
|
26
35
|
|
|
27
36
|
references {
|
|
28
|
-
|
|
29
|
-
repository { 'amaranthine' }
|
|
30
|
-
paths { '/data/items/*.yaml' }
|
|
31
|
-
}
|
|
37
|
+
ItemData
|
|
32
38
|
}
|
|
33
39
|
}
|
|
34
40
|
|