@sprig-and-prose/sprig-universe 0.4.3 → 0.4.4
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/universe/.sprig/manifest.json +1941 -0
- package/src/universe/graph.js +340 -6
- package/src/universe/parser.js +49 -0
- package/src/universe/scanner.js +1 -0
- package/src/universe/universe.prose +17 -10
package/src/universe/graph.js
CHANGED
|
@@ -920,6 +920,21 @@ function getRawText(contentSpan, sourceText) {
|
|
|
920
920
|
return sourceText.slice(contentSpan.startOffset, contentSpan.endOffset);
|
|
921
921
|
}
|
|
922
922
|
|
|
923
|
+
/**
|
|
924
|
+
* @param {string} base
|
|
925
|
+
* @param {string} path
|
|
926
|
+
* @returns {string}
|
|
927
|
+
*/
|
|
928
|
+
function joinRepositoryUrl(base, path) {
|
|
929
|
+
if (base.endsWith('/') && path.startsWith('/')) {
|
|
930
|
+
return base + path.slice(1);
|
|
931
|
+
}
|
|
932
|
+
if (!base.endsWith('/') && !path.startsWith('/')) {
|
|
933
|
+
return `${base}/${path}`;
|
|
934
|
+
}
|
|
935
|
+
return base + path;
|
|
936
|
+
}
|
|
937
|
+
|
|
923
938
|
/**
|
|
924
939
|
* Pass 4: Build nodes
|
|
925
940
|
* @param {BoundDecl[]} boundDecls
|
|
@@ -1078,7 +1093,7 @@ function buildNodes(boundDecls, schemas, fileData) {
|
|
|
1078
1093
|
// Extract unknown blocks (non-container, non-standard blocks)
|
|
1079
1094
|
const unknownBlocks = (decl.body || []).filter(b => {
|
|
1080
1095
|
if (!b.kind) return false;
|
|
1081
|
-
const knownKinds = ['describe', 'title', 'note', 'relationships', 'from', 'references', 'reference'];
|
|
1096
|
+
const knownKinds = ['describe', 'title', 'note', 'relationships', 'from', 'references', 'reference', 'ordering'];
|
|
1082
1097
|
return !knownKinds.includes(b.kind);
|
|
1083
1098
|
});
|
|
1084
1099
|
if (unknownBlocks.length > 0) {
|
|
@@ -1090,13 +1105,28 @@ function buildNodes(boundDecls, schemas, fileData) {
|
|
|
1090
1105
|
}));
|
|
1091
1106
|
}
|
|
1092
1107
|
|
|
1093
|
-
// Extract references block
|
|
1108
|
+
// Extract references block (resolved after all nodes are built)
|
|
1094
1109
|
const referencesBlock = (decl.body || []).find(b => b.kind === 'references');
|
|
1095
1110
|
if (referencesBlock && referencesBlock.items) {
|
|
1096
|
-
node.
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1111
|
+
node._pendingReferences = referencesBlock.items
|
|
1112
|
+
.map(item => ({
|
|
1113
|
+
name: item.ref || item.name || '',
|
|
1114
|
+
source: item.span || referencesBlock.source,
|
|
1115
|
+
}))
|
|
1116
|
+
.filter(item => item.name);
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// Extract ordering block (applied after all nodes are built)
|
|
1120
|
+
const orderingBlocks = (decl.body || []).filter(b => b.kind === 'ordering');
|
|
1121
|
+
if (orderingBlocks.length > 0) {
|
|
1122
|
+
if (orderingBlocks.length > 1) {
|
|
1123
|
+
diagnostics.push({
|
|
1124
|
+
severity: 'warning',
|
|
1125
|
+
message: `Multiple ordering blocks in ${node.kind} "${node.name}". Using the first one.`,
|
|
1126
|
+
source: orderingBlocks[1].source || orderingBlocks[0].source,
|
|
1127
|
+
});
|
|
1128
|
+
}
|
|
1129
|
+
node._pendingOrdering = orderingBlocks[0];
|
|
1100
1130
|
}
|
|
1101
1131
|
|
|
1102
1132
|
nodes.set(id, node);
|
|
@@ -1172,6 +1202,12 @@ function buildNodes(boundDecls, schemas, fileData) {
|
|
|
1172
1202
|
if (pathsBlock && pathsBlock.paths) {
|
|
1173
1203
|
ref.paths = pathsBlock.paths;
|
|
1174
1204
|
}
|
|
1205
|
+
|
|
1206
|
+
// Extract kind block
|
|
1207
|
+
const kindBlock = (decl.body || []).find(b => b.kind === 'kind');
|
|
1208
|
+
if (kindBlock && kindBlock.value) {
|
|
1209
|
+
ref.kind = kindBlock.value;
|
|
1210
|
+
}
|
|
1175
1211
|
|
|
1176
1212
|
// Extract title block
|
|
1177
1213
|
const titleBlock = (decl.body || []).find(b => b.kind === 'title');
|
|
@@ -1202,6 +1238,293 @@ function buildNodes(boundDecls, schemas, fileData) {
|
|
|
1202
1238
|
return { nodes, repositories, references, diagnostics };
|
|
1203
1239
|
}
|
|
1204
1240
|
|
|
1241
|
+
/**
|
|
1242
|
+
* Attach named references declared inside containers to their parent nodes.
|
|
1243
|
+
* @param {BoundDecl[]} boundDecls
|
|
1244
|
+
* @param {Map<DeclId, NodeModel>} nodes
|
|
1245
|
+
*/
|
|
1246
|
+
function addImplicitReferenceAttachments(boundDecls, nodes) {
|
|
1247
|
+
for (const bound of boundDecls) {
|
|
1248
|
+
const { info } = bound;
|
|
1249
|
+
if (info.kindResolved !== 'reference') continue;
|
|
1250
|
+
const parentId = info.parentDeclId;
|
|
1251
|
+
if (!parentId) continue;
|
|
1252
|
+
const parentNode = nodes.get(parentId);
|
|
1253
|
+
if (!parentNode || parentNode.kind === 'universe') continue;
|
|
1254
|
+
if (!parentNode._pendingReferences) {
|
|
1255
|
+
parentNode._pendingReferences = [];
|
|
1256
|
+
}
|
|
1257
|
+
parentNode._pendingReferences.push({
|
|
1258
|
+
name: info.name,
|
|
1259
|
+
source: info.span,
|
|
1260
|
+
});
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
/**
|
|
1265
|
+
* Resolve reference declarations to repository URLs and repository references.
|
|
1266
|
+
* @param {BoundDecl[]} boundDecls
|
|
1267
|
+
* @param {Map<DeclId, ReferenceModel>} references
|
|
1268
|
+
* @param {Map<DeclId, RepositoryModel>} repositories
|
|
1269
|
+
* @param {DeclIndex} index
|
|
1270
|
+
* @param {Map<DeclId, DeclId>} parentMap
|
|
1271
|
+
* @returns {Diagnostic[]}
|
|
1272
|
+
*/
|
|
1273
|
+
function resolveReferenceModels(boundDecls, references, repositories, index, parentMap) {
|
|
1274
|
+
const diagnostics = [];
|
|
1275
|
+
const unknownRepoCounts = new Map();
|
|
1276
|
+
|
|
1277
|
+
for (const bound of boundDecls) {
|
|
1278
|
+
const { id, info } = bound;
|
|
1279
|
+
if (info.kindResolved !== 'reference') continue;
|
|
1280
|
+
const ref = references.get(id);
|
|
1281
|
+
if (!ref) continue;
|
|
1282
|
+
|
|
1283
|
+
const displayName = ref.name || info.name || 'unnamed reference';
|
|
1284
|
+
const hasUrl = Array.isArray(ref.urls) && ref.urls.length > 0;
|
|
1285
|
+
const repositoryName = ref.repositoryName;
|
|
1286
|
+
let urls = hasUrl ? [...ref.urls] : [];
|
|
1287
|
+
let repositoryRef = undefined;
|
|
1288
|
+
|
|
1289
|
+
if (hasUrl && repositoryName) {
|
|
1290
|
+
diagnostics.push({
|
|
1291
|
+
severity: 'error',
|
|
1292
|
+
message: `Reference "${displayName}" cannot include both url { ... } and in <Repository>`,
|
|
1293
|
+
source: info.span,
|
|
1294
|
+
});
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
if (!hasUrl && repositoryName) {
|
|
1298
|
+
const pathSegments = repositoryName.split('.');
|
|
1299
|
+
if (pathSegments.length === 1) {
|
|
1300
|
+
const resolved = resolveUnqualified(pathSegments[0], id, index, parentMap, info.span);
|
|
1301
|
+
if (resolved.ambiguous) {
|
|
1302
|
+
diagnostics.push({
|
|
1303
|
+
severity: 'error',
|
|
1304
|
+
message: `Ambiguous repository "${repositoryName}" - use qualified path`,
|
|
1305
|
+
source: info.span,
|
|
1306
|
+
});
|
|
1307
|
+
} else if (resolved.declId) {
|
|
1308
|
+
const repo = repositories.get(resolved.declId);
|
|
1309
|
+
if (!repo) {
|
|
1310
|
+
diagnostics.push({
|
|
1311
|
+
severity: 'error',
|
|
1312
|
+
message: `Reference "${displayName}" uses "${repositoryName}" which is not a repository`,
|
|
1313
|
+
source: info.span,
|
|
1314
|
+
});
|
|
1315
|
+
} else if (!repo.url) {
|
|
1316
|
+
diagnostics.push({
|
|
1317
|
+
severity: 'error',
|
|
1318
|
+
message: `Repository "${repo.name}" is missing url { '...' }`,
|
|
1319
|
+
source: info.span,
|
|
1320
|
+
});
|
|
1321
|
+
} else {
|
|
1322
|
+
repositoryRef = repo.id;
|
|
1323
|
+
if (ref.paths && ref.paths.length > 0) {
|
|
1324
|
+
urls = ref.paths.map((path) => joinRepositoryUrl(repo.url, path));
|
|
1325
|
+
} else if (ref.paths && ref.paths.length === 0) {
|
|
1326
|
+
diagnostics.push({
|
|
1327
|
+
severity: 'error',
|
|
1328
|
+
message: `Reference "${displayName}" has an empty paths block`,
|
|
1329
|
+
source: info.span,
|
|
1330
|
+
});
|
|
1331
|
+
} else {
|
|
1332
|
+
urls = [repo.url];
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
} else {
|
|
1336
|
+
const entry = unknownRepoCounts.get(repositoryName) || {
|
|
1337
|
+
count: 0,
|
|
1338
|
+
source: info.span,
|
|
1339
|
+
};
|
|
1340
|
+
entry.count += 1;
|
|
1341
|
+
if (!entry.source && info.span) {
|
|
1342
|
+
entry.source = info.span;
|
|
1343
|
+
}
|
|
1344
|
+
unknownRepoCounts.set(repositoryName, entry);
|
|
1345
|
+
}
|
|
1346
|
+
} else {
|
|
1347
|
+
const resolved = resolveQualified(pathSegments, index, info.span);
|
|
1348
|
+
if (resolved.declId) {
|
|
1349
|
+
const repo = repositories.get(resolved.declId);
|
|
1350
|
+
if (!repo) {
|
|
1351
|
+
diagnostics.push({
|
|
1352
|
+
severity: 'error',
|
|
1353
|
+
message: `Reference "${displayName}" uses "${repositoryName}" which is not a repository`,
|
|
1354
|
+
source: info.span,
|
|
1355
|
+
});
|
|
1356
|
+
} else if (!repo.url) {
|
|
1357
|
+
diagnostics.push({
|
|
1358
|
+
severity: 'error',
|
|
1359
|
+
message: `Repository "${repo.name}" is missing url { '...' }`,
|
|
1360
|
+
source: info.span,
|
|
1361
|
+
});
|
|
1362
|
+
} else {
|
|
1363
|
+
repositoryRef = repo.id;
|
|
1364
|
+
if (ref.paths && ref.paths.length > 0) {
|
|
1365
|
+
urls = ref.paths.map((path) => joinRepositoryUrl(repo.url, path));
|
|
1366
|
+
} else if (ref.paths && ref.paths.length === 0) {
|
|
1367
|
+
diagnostics.push({
|
|
1368
|
+
severity: 'error',
|
|
1369
|
+
message: `Reference "${displayName}" has an empty paths block`,
|
|
1370
|
+
source: info.span,
|
|
1371
|
+
});
|
|
1372
|
+
} else {
|
|
1373
|
+
urls = [repo.url];
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
} else {
|
|
1377
|
+
const entry = unknownRepoCounts.get(repositoryName) || {
|
|
1378
|
+
count: 0,
|
|
1379
|
+
source: info.span,
|
|
1380
|
+
};
|
|
1381
|
+
entry.count += 1;
|
|
1382
|
+
if (!entry.source && info.span) {
|
|
1383
|
+
entry.source = info.span;
|
|
1384
|
+
}
|
|
1385
|
+
unknownRepoCounts.set(repositoryName, entry);
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
if (!hasUrl && !repositoryName) {
|
|
1391
|
+
diagnostics.push({
|
|
1392
|
+
severity: 'error',
|
|
1393
|
+
message: `Reference "${displayName}" must have url { ... } or declared as reference ${displayName} in <Repository>`,
|
|
1394
|
+
source: info.span,
|
|
1395
|
+
});
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
if (urls.length > 0) {
|
|
1399
|
+
ref.urls = urls;
|
|
1400
|
+
}
|
|
1401
|
+
if (repositoryRef) {
|
|
1402
|
+
ref.repositoryRef = repositoryRef;
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
for (const [repoName, info] of unknownRepoCounts.entries()) {
|
|
1407
|
+
const countText = info.count > 1 ? ` (${info.count} occurrences)` : '';
|
|
1408
|
+
diagnostics.push({
|
|
1409
|
+
severity: 'error',
|
|
1410
|
+
message: `Unknown repository "${repoName}" used by references${countText}.`,
|
|
1411
|
+
source: info.source,
|
|
1412
|
+
});
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
return diagnostics;
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
/**
|
|
1419
|
+
* Resolve references blocks to reference IDs.
|
|
1420
|
+
* @param {Map<DeclId, NodeModel>} nodes
|
|
1421
|
+
* @param {Map<DeclId, ReferenceModel>} references
|
|
1422
|
+
* @param {DeclIndex} index
|
|
1423
|
+
* @param {Map<DeclId, DeclId>} parentMap
|
|
1424
|
+
* @returns {Diagnostic[]}
|
|
1425
|
+
*/
|
|
1426
|
+
function resolveReferenceAttachments(nodes, references, index, parentMap) {
|
|
1427
|
+
const diagnostics = [];
|
|
1428
|
+
const unknownReferenceCounts = new Map();
|
|
1429
|
+
|
|
1430
|
+
for (const node of nodes.values()) {
|
|
1431
|
+
if (!node._pendingReferences) continue;
|
|
1432
|
+
const resolvedReferences = [];
|
|
1433
|
+
|
|
1434
|
+
for (const item of node._pendingReferences) {
|
|
1435
|
+
const name = item.name;
|
|
1436
|
+
if (!name) continue;
|
|
1437
|
+
|
|
1438
|
+
let resolvedDeclId = null;
|
|
1439
|
+
const pathSegments = name.split('.');
|
|
1440
|
+
if (pathSegments.length === 1) {
|
|
1441
|
+
const resolved = resolveUnqualified(pathSegments[0], node.id, index, parentMap, item.source);
|
|
1442
|
+
if (resolved.ambiguous) {
|
|
1443
|
+
diagnostics.push({
|
|
1444
|
+
severity: 'error',
|
|
1445
|
+
message: `Ambiguous reference "${name}" - use qualified path`,
|
|
1446
|
+
source: item.source,
|
|
1447
|
+
});
|
|
1448
|
+
continue;
|
|
1449
|
+
}
|
|
1450
|
+
resolvedDeclId = resolved.declId;
|
|
1451
|
+
} else {
|
|
1452
|
+
const resolved = resolveQualified(pathSegments, index, item.source);
|
|
1453
|
+
resolvedDeclId = resolved.declId;
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
if (resolvedDeclId && references.has(resolvedDeclId)) {
|
|
1457
|
+
resolvedReferences.push(resolvedDeclId);
|
|
1458
|
+
} else {
|
|
1459
|
+
const entry = unknownReferenceCounts.get(name) || { count: 0, source: item.source };
|
|
1460
|
+
entry.count += 1;
|
|
1461
|
+
if (!entry.source && item.source) {
|
|
1462
|
+
entry.source = item.source;
|
|
1463
|
+
}
|
|
1464
|
+
unknownReferenceCounts.set(name, entry);
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
node.references = resolvedReferences;
|
|
1469
|
+
delete node._pendingReferences;
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
for (const [name, info] of unknownReferenceCounts.entries()) {
|
|
1473
|
+
const countText = info.count > 1 ? ` (${info.count} occurrences)` : '';
|
|
1474
|
+
diagnostics.push({
|
|
1475
|
+
severity: 'error',
|
|
1476
|
+
message: `Unknown reference "${name}" in references list${countText}. References must use reference names, not paths.`,
|
|
1477
|
+
source: info.source,
|
|
1478
|
+
});
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
return diagnostics;
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
/**
|
|
1485
|
+
* Validate and apply ordering blocks after all nodes are created.
|
|
1486
|
+
* @param {Map<DeclId, NodeModel>} nodes
|
|
1487
|
+
* @returns {Diagnostic[]}
|
|
1488
|
+
*/
|
|
1489
|
+
function applyOrderingBlocks(nodes) {
|
|
1490
|
+
const diagnostics = [];
|
|
1491
|
+
|
|
1492
|
+
for (const node of nodes.values()) {
|
|
1493
|
+
if (!node._pendingOrdering) continue;
|
|
1494
|
+
const orderingBlock = node._pendingOrdering;
|
|
1495
|
+
const orderingIdentifiers = orderingBlock.identifiers || [];
|
|
1496
|
+
|
|
1497
|
+
const childNameToNodeId = new Map();
|
|
1498
|
+
for (const childId of node.children || []) {
|
|
1499
|
+
const childNode = nodes.get(childId);
|
|
1500
|
+
if (childNode) {
|
|
1501
|
+
childNameToNodeId.set(childNode.name, childId);
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
const validOrdering = [];
|
|
1506
|
+
for (const identifier of orderingIdentifiers) {
|
|
1507
|
+
if (childNameToNodeId.has(identifier)) {
|
|
1508
|
+
validOrdering.push(identifier);
|
|
1509
|
+
} else {
|
|
1510
|
+
diagnostics.push({
|
|
1511
|
+
severity: 'warning',
|
|
1512
|
+
message: `Ordering block in ${node.kind} "${node.name}" references unknown child "${identifier}". This identifier will be ignored.`,
|
|
1513
|
+
source: orderingBlock.source,
|
|
1514
|
+
});
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
if (validOrdering.length > 0) {
|
|
1519
|
+
node.ordering = validOrdering;
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
delete node._pendingOrdering;
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
return diagnostics;
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1205
1528
|
/**
|
|
1206
1529
|
* Pass 5: Build edges from relationship usage
|
|
1207
1530
|
* @param {BoundDecl[]} boundDecls
|
|
@@ -1467,6 +1790,14 @@ export function buildGraph(fileAstOrFileAsts, options) {
|
|
|
1467
1790
|
// Pass 4: Build nodes
|
|
1468
1791
|
const fileData = normalized.map(n => ({ sourceText: n.sourceText, filePath: n.filePath }));
|
|
1469
1792
|
const { nodes, repositories, references, diagnostics: nodeDiags } = buildNodes(boundDecls, schemas, fileData);
|
|
1793
|
+
|
|
1794
|
+
// Resolve reference models and attachments
|
|
1795
|
+
addImplicitReferenceAttachments(boundDecls, nodes);
|
|
1796
|
+
const referenceModelDiags = resolveReferenceModels(boundDecls, references, repositories, index, parentMap);
|
|
1797
|
+
const referenceAttachmentDiags = resolveReferenceAttachments(nodes, references, index, parentMap);
|
|
1798
|
+
|
|
1799
|
+
// Apply ordering blocks after all nodes are available
|
|
1800
|
+
const orderingDiags = applyOrderingBlocks(nodes);
|
|
1470
1801
|
|
|
1471
1802
|
// Resolve relates endpoints
|
|
1472
1803
|
const relatesDiags = resolveRelatesEndpoints(nodes, index, parentMap, boundDecls);
|
|
@@ -1483,6 +1814,9 @@ export function buildGraph(fileAstOrFileAsts, options) {
|
|
|
1483
1814
|
...bindDiags,
|
|
1484
1815
|
...schemaDiags,
|
|
1485
1816
|
...nodeDiags,
|
|
1817
|
+
...referenceModelDiags,
|
|
1818
|
+
...referenceAttachmentDiags,
|
|
1819
|
+
...orderingDiags,
|
|
1486
1820
|
...relatesDiags,
|
|
1487
1821
|
...edgeDiags,
|
|
1488
1822
|
];
|
package/src/universe/parser.js
CHANGED
|
@@ -342,6 +342,7 @@ const blockParselets = new Map([
|
|
|
342
342
|
['label', (p) => p.parseLabelBlock()],
|
|
343
343
|
['reference', (p) => p.parseReferenceBlock()],
|
|
344
344
|
['references', (p) => p.parseReferencesBlock()],
|
|
345
|
+
['ordering', (p) => p.parseOrderingBlock()],
|
|
345
346
|
['relationships', (p) => p.parseRelationshipsBlock()],
|
|
346
347
|
]);
|
|
347
348
|
|
|
@@ -1074,6 +1075,54 @@ ParserCore.prototype.parseReferenceBlock = function () {
|
|
|
1074
1075
|
};
|
|
1075
1076
|
};
|
|
1076
1077
|
|
|
1078
|
+
/**
|
|
1079
|
+
* Parses an ordering block: ordering { <identifiers> }
|
|
1080
|
+
* Items are identifiers (not paths).
|
|
1081
|
+
* @returns {any | null}
|
|
1082
|
+
*/
|
|
1083
|
+
ParserCore.prototype.parseOrderingBlock = function () {
|
|
1084
|
+
const startToken = this.peek();
|
|
1085
|
+
if (!startToken) return null;
|
|
1086
|
+
|
|
1087
|
+
const { token: orderingToken } = this.expectIdentifierOrKeyword('ordering');
|
|
1088
|
+
if (!orderingToken) return null;
|
|
1089
|
+
|
|
1090
|
+
const { token: lbrace } = this.expect('LBRACE');
|
|
1091
|
+
if (!lbrace) return null;
|
|
1092
|
+
|
|
1093
|
+
const identifiers = [];
|
|
1094
|
+
|
|
1095
|
+
// Parse items until closing brace
|
|
1096
|
+
while (!this.isAtEnd() && !this.match('RBRACE')) {
|
|
1097
|
+
// Skip optional commas
|
|
1098
|
+
if (this.match('COMMA')) {
|
|
1099
|
+
this.advance();
|
|
1100
|
+
continue;
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
const itemToken = this.peek();
|
|
1104
|
+
if (!itemToken) break;
|
|
1105
|
+
|
|
1106
|
+
const identifier = this.readIdent();
|
|
1107
|
+
if (!identifier) {
|
|
1108
|
+
this.reportDiagnostic('error', 'Expected identifier in ordering block', itemToken.span);
|
|
1109
|
+
this.advance();
|
|
1110
|
+
continue;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
identifiers.push(identifier);
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
const { token: rbrace } = this.expect('RBRACE');
|
|
1117
|
+
if (!rbrace) return null;
|
|
1118
|
+
|
|
1119
|
+
return {
|
|
1120
|
+
kind: 'ordering',
|
|
1121
|
+
identifiers,
|
|
1122
|
+
source: this.createSpan(orderingToken, rbrace),
|
|
1123
|
+
};
|
|
1124
|
+
};
|
|
1125
|
+
|
|
1077
1126
|
/**
|
|
1078
1127
|
* Parses a references block: references { <items> }
|
|
1079
1128
|
* Items are identifier paths (reference names)
|
package/src/universe/scanner.js
CHANGED
|
@@ -67,14 +67,16 @@ anthology AnthologyWithRelationships in CanonicalUniverse {
|
|
|
67
67
|
A chapter can still be nested under an aliased book.
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
relationships {
|
|
71
|
+
owns {
|
|
72
|
+
ExternalOtherChapter,
|
|
73
|
+
ExternalBook,
|
|
74
|
+
}
|
|
74
75
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
singleton {
|
|
77
|
+
ExternalAnthology
|
|
78
|
+
ExternalSeries
|
|
79
|
+
}
|
|
78
80
|
}
|
|
79
81
|
}
|
|
80
82
|
|
|
@@ -104,21 +106,21 @@ anthology AnthologyWithRelationships in CanonicalUniverse {
|
|
|
104
106
|
}
|
|
105
107
|
|
|
106
108
|
reference NamedReference1 {
|
|
107
|
-
url { 'https://github.com/owner/repository/tree/main' }
|
|
109
|
+
url { 'https://github.com/owner/repository/tree/main/references/reference1.md' }
|
|
108
110
|
describe {
|
|
109
111
|
References can have describe blocks.
|
|
110
112
|
}
|
|
111
113
|
}
|
|
112
114
|
|
|
113
115
|
reference NamedReference2 {
|
|
114
|
-
url { 'https://github.com/owner/repository/tree/main' }
|
|
116
|
+
url { 'https://github.com/owner/repository/tree/main/references/reference2.md' }
|
|
115
117
|
describe {
|
|
116
118
|
References can have describe blocks.
|
|
117
119
|
}
|
|
118
120
|
}
|
|
119
121
|
|
|
120
122
|
reference NamedReferenceInRepository in InternalRepository {
|
|
121
|
-
|
|
123
|
+
paths { 'path/to/file1.md' 'path/to/file2.md' }
|
|
122
124
|
}
|
|
123
125
|
|
|
124
126
|
concept ConceptWithDirectReference {
|
|
@@ -150,6 +152,11 @@ anthology ExternalAnthology in CanonicalUniverse {
|
|
|
150
152
|
|
|
151
153
|
bookAlias BookAlias {
|
|
152
154
|
chapterAlias NestedChapterAlias { }
|
|
155
|
+
|
|
156
|
+
ordering {
|
|
157
|
+
NestedChapterAlias
|
|
158
|
+
ExternalChapterAlias
|
|
159
|
+
}
|
|
153
160
|
}
|
|
154
161
|
|
|
155
162
|
chapterAlias ExternalChapterAlias in BookAlias { }
|