@oml/language 0.13.0 → 0.14.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.
Files changed (41) hide show
  1. package/out/oml/index.d.ts +3 -1
  2. package/out/oml/index.js +19 -1
  3. package/out/oml/index.js.map +1 -1
  4. package/out/oml/oml-candidates.d.ts +3 -3
  5. package/out/oml/oml-candidates.js +1 -1
  6. package/out/oml/oml-candidates.js.map +1 -1
  7. package/out/oml/oml-diagram.d.ts +17 -0
  8. package/out/oml/oml-diagram.js +1549 -0
  9. package/out/oml/oml-diagram.js.map +1 -0
  10. package/out/oml/oml-document.d.ts +5 -0
  11. package/out/oml/oml-document.js +12 -1
  12. package/out/oml/oml-document.js.map +1 -1
  13. package/out/oml/oml-index.d.ts +2 -1
  14. package/out/oml/oml-index.js +90 -9
  15. package/out/oml/oml-index.js.map +1 -1
  16. package/out/oml/oml-scope.js +4 -3
  17. package/out/oml/oml-scope.js.map +1 -1
  18. package/out/oml/oml-search.d.ts +24 -0
  19. package/out/oml/oml-search.js +95 -0
  20. package/out/oml/oml-search.js.map +1 -0
  21. package/out/oml/{oml-edit.d.ts → oml-update.d.ts} +2 -0
  22. package/out/oml/{oml-edit.js → oml-update.js} +256 -56
  23. package/out/oml/oml-update.js.map +1 -0
  24. package/out/oml/oml-utils.d.ts +1 -1
  25. package/out/oml/oml-utils.js +3 -4
  26. package/out/oml/oml-utils.js.map +1 -1
  27. package/out/oml/oml-validator.d.ts +1 -0
  28. package/out/oml/oml-validator.js +17 -2
  29. package/out/oml/oml-validator.js.map +1 -1
  30. package/package.json +4 -2
  31. package/src/oml/index.ts +32 -1
  32. package/src/oml/oml-candidates.ts +4 -4
  33. package/src/oml/oml-diagram.ts +1708 -0
  34. package/src/oml/oml-document.ts +13 -1
  35. package/src/oml/oml-index.ts +87 -9
  36. package/src/oml/oml-scope.ts +4 -3
  37. package/src/oml/oml-search.ts +132 -0
  38. package/src/oml/{oml-edit.ts → oml-update.ts} +302 -55
  39. package/src/oml/oml-utils.ts +3 -4
  40. package/src/oml/oml-validator.ts +17 -2
  41. package/out/oml/oml-edit.js.map +0 -1
@@ -0,0 +1,1708 @@
1
+ // Copyright (c) 2026 Modelware. All rights reserved.
2
+
3
+ import * as ElkModule from 'elkjs/lib/elk.bundled.js';
4
+ import { URI } from 'langium';
5
+ import type { AstNode, LangiumDocument } from 'langium';
6
+ import type { SModelElement, SModelRoot } from 'sprotty-protocol';
7
+ import {
8
+ isAspect,
9
+ isConcept,
10
+ isConceptInstance,
11
+ isDescription,
12
+ isDescriptionBundle,
13
+ isEquivalenceAxiom,
14
+ isImport,
15
+ isInstanceEnumerationAxiom,
16
+ isLiteralEnumerationAxiom,
17
+ isOntology,
18
+ isPropertyValueAssertion,
19
+ isRelation,
20
+ isRelationEntity,
21
+ isRelationInstance,
22
+ isScalar,
23
+ isScalarProperty,
24
+ isSpecializationAxiom,
25
+ isUnreifiedRelation,
26
+ isVocabulary,
27
+ isVocabularyBundle,
28
+ type Import,
29
+ type Ontology
30
+ } from './generated/ast.js';
31
+ import { getOntologyModelIndex } from './oml-index.js';
32
+ import {
33
+ collectOntologyMembers,
34
+ findOntologyMemberByName,
35
+ getIriForNode,
36
+ getModelIdForNode,
37
+ getNamedElementName,
38
+ humanizeTypeName,
39
+ normalizeNamespace,
40
+ splitIri
41
+ } from './oml-utils.js';
42
+
43
+ export type RangeResponse = {
44
+ uri: string;
45
+ startLine: number;
46
+ startColumn: number;
47
+ endLine: number;
48
+ endColumn: number;
49
+ };
50
+
51
+ type DiagramEntry = {
52
+ id: string;
53
+ modelId: string;
54
+ text: string;
55
+ qualifiedText?: string;
56
+ tooltip?: string;
57
+ statement?: AstNode;
58
+ };
59
+
60
+ type DiagramNode = {
61
+ id: string;
62
+ viewId: string;
63
+ modelId: string;
64
+ iri?: string;
65
+ label: string;
66
+ instanceTypeLabels?: string[];
67
+ qualifiedLabel?: string;
68
+ tooltip?: string;
69
+ propertyEntries?: DiagramEntry[];
70
+ compartmentEntries?: DiagramEntry[];
71
+ kind: 'entity' | 'instance' | 'ontology' | 'relation' | 'equivalence';
72
+ statement?: AstNode;
73
+ };
74
+
75
+ type DiagramEdge = {
76
+ id: string;
77
+ viewId: string;
78
+ modelId: string;
79
+ iri?: string;
80
+ sourceId: string;
81
+ targetId: string;
82
+ label: string;
83
+ qualifiedLabel?: string;
84
+ tooltip?: string;
85
+ kind: 'relation' | 'relation-source' | 'relation-target' | 'specialization' | 'equivalence' | 'equivalence-source' | 'import';
86
+ statement?: AstNode;
87
+ };
88
+
89
+ type DiagramGraph = {
90
+ rootUri: string;
91
+ root: Ontology;
92
+ nodes: DiagramNode[];
93
+ edges: DiagramEdge[];
94
+ statementsById: Map<string, AstNode>;
95
+ };
96
+
97
+ type AstRef = {
98
+ uri: string;
99
+ path: string;
100
+ };
101
+
102
+ type DiagramBuildContext = {
103
+ root: Ontology;
104
+ nsToPrefix: Map<string, string>;
105
+ };
106
+
107
+ const ElkConstructor = (ElkModule as any).default || ElkModule;
108
+ let elkPromise: Promise<any> | undefined;
109
+ const navigationIndex = new Map<string, Map<string, AstRef>>();
110
+
111
+ async function getElk(): Promise<any> {
112
+ if (!elkPromise) {
113
+ elkPromise = (async () => {
114
+ try {
115
+ const workerModule = await import('elkjs/lib/elk-worker.js');
116
+ const ElkWorkerCtor = (workerModule as any).Worker
117
+ || (workerModule as any).default?.Worker
118
+ || (workerModule as any).default
119
+ || workerModule;
120
+ return new ElkConstructor({
121
+ algorithms: ['layered'],
122
+ workerFactory: (url?: string) => new ElkWorkerCtor(url) as unknown
123
+ });
124
+ } catch (error) {
125
+ console.error('[OML Diagram] Failed to initialize ELK:', error);
126
+ return {
127
+ layout: async (graph: any) => graph
128
+ };
129
+ }
130
+ })();
131
+ }
132
+ return await elkPromise;
133
+ }
134
+
135
+ function getAstNodeLocator(shared: any): { getAstNodePath(node: any): string; getAstNode(root: any, path: string): any } | undefined {
136
+ const isLocator = (candidate: any): boolean =>
137
+ !!candidate
138
+ && typeof candidate.getAstNodePath === 'function'
139
+ && typeof candidate.getAstNode === 'function';
140
+
141
+ const direct = shared?.workspace?.AstNodeLocator ?? shared?.AstNodeLocator;
142
+ if (isLocator(direct)) {
143
+ return direct;
144
+ }
145
+
146
+ const registry: any = shared?.ServiceRegistry;
147
+ if (!registry) {
148
+ return undefined;
149
+ }
150
+ const all = registry.all;
151
+ const services = Array.isArray(all)
152
+ ? all
153
+ : (typeof all?.toArray === 'function' ? all.toArray() : Array.from((all ?? []) as Iterable<any>));
154
+ for (const languageServices of services) {
155
+ const locator = languageServices?.workspace?.AstNodeLocator ?? languageServices?.AstNodeLocator;
156
+ if (isLocator(locator)) {
157
+ return locator;
158
+ }
159
+ }
160
+ return undefined;
161
+ }
162
+
163
+ function buildDiagramContext(root: Ontology): DiagramBuildContext {
164
+ const nsToPrefix = new Map<string, string>();
165
+ for (const ownedImport of (((root as any).ownedImports ?? []) as Import[])) {
166
+ if (!isImport(ownedImport)) continue;
167
+ const importedOntology = (ownedImport as any).imported?.ref;
168
+ const prefix = (ownedImport as any).prefix;
169
+ if (!importedOntology || !prefix) continue;
170
+ const ns = normalizeNamespace(String((importedOntology as any).namespace ?? '').replace(/^<|>$/g, ''));
171
+ if (!ns) continue;
172
+ nsToPrefix.set(ns, prefix);
173
+ }
174
+ return { root, nsToPrefix };
175
+ }
176
+
177
+ function getContainingOntology(astNode: any): any {
178
+ let current = astNode;
179
+ while (current && !isOntology(current)) {
180
+ current = current.$container;
181
+ }
182
+ return current;
183
+ }
184
+
185
+ function unwrapReferenceValue(candidate: any): any {
186
+ if (!candidate || typeof candidate !== 'object') {
187
+ return candidate;
188
+ }
189
+ if ('$refText' in candidate && 'ref' in candidate) {
190
+ return (candidate as any).ref;
191
+ }
192
+ return candidate;
193
+ }
194
+
195
+ function getQualifiedName(ctx: DiagramBuildContext, astNode: any): string | undefined {
196
+ const value = unwrapReferenceValue(astNode);
197
+ if (!value) {
198
+ return undefined;
199
+ }
200
+
201
+ const iri = getIriForNode(value);
202
+ if (iri) {
203
+ const parts = splitIri(iri);
204
+ if (parts?.fragment) {
205
+ const prefix = ctx.nsToPrefix.get(normalizeNamespace(parts.base));
206
+ return prefix ? `${prefix}:${parts.fragment}` : parts.fragment;
207
+ }
208
+ }
209
+
210
+ if (isOntology(value)) {
211
+ const namespace = normalizeNamespace(String((value as any).namespace ?? '').replace(/^<|>$/g, ''));
212
+ if (!namespace) {
213
+ return undefined;
214
+ }
215
+ const prefix = (value as any).prefix;
216
+ if (typeof prefix === 'string' && prefix.length > 0) {
217
+ return prefix;
218
+ }
219
+ return namespace;
220
+ }
221
+
222
+ const name = getNamedElementName(value);
223
+ if (!name) {
224
+ return undefined;
225
+ }
226
+
227
+ const owningOntology = getContainingOntology(value);
228
+ const namespace = normalizeNamespace(String((owningOntology as any)?.namespace ?? '').replace(/^<|>$/g, ''));
229
+ const prefix = namespace ? ctx.nsToPrefix.get(namespace) : undefined;
230
+ return prefix ? `${prefix}:${name}` : name;
231
+ }
232
+
233
+ function getRefDisplayName(ctx: DiagramBuildContext, refLike: any): string | undefined {
234
+ const direct = getQualifiedName(ctx, refLike?.ref ?? refLike);
235
+ if (direct) {
236
+ return direct;
237
+ }
238
+ const raw = typeof refLike?.$refText === 'string' ? refLike.$refText.trim() : '';
239
+ if (!raw) {
240
+ return undefined;
241
+ }
242
+ const normalized = raw.replace(/^<|>$/g, '');
243
+ const parts = splitIri(normalized);
244
+ if (!parts?.fragment) {
245
+ return normalized;
246
+ }
247
+ const prefix = ctx.nsToPrefix.get(normalizeNamespace(parts.base));
248
+ return prefix ? `${prefix}:${parts.fragment}` : parts.fragment;
249
+ }
250
+
251
+ function toSimpleName(label: string | undefined): string {
252
+ if (!label) return '';
253
+ const trimmed = label.trim();
254
+ if (!trimmed) return '';
255
+ const colonIndex = trimmed.indexOf(':');
256
+ if (colonIndex >= 0 && colonIndex < trimmed.length - 1) {
257
+ return trimmed.slice(colonIndex + 1);
258
+ }
259
+ const parts = splitIri(trimmed);
260
+ return parts?.fragment || trimmed;
261
+ }
262
+
263
+ function truncateWithEllipsis(text: string, maxChars: number): string {
264
+ if (maxChars <= 0) return '';
265
+ if (text.length <= maxChars) return text;
266
+ if (maxChars === 1) return '…';
267
+ return `${text.slice(0, maxChars - 1)}…`;
268
+ }
269
+
270
+ function iriNamespace(iri: string | undefined): string | undefined {
271
+ if (!iri) return undefined;
272
+ const parts = splitIri(iri);
273
+ return parts?.base;
274
+ }
275
+
276
+ function formatHoverLikeTooltip(kind: string, qualified: string | undefined, iri?: string): string | undefined {
277
+ const parts = [kind];
278
+ if (qualified && qualified.length > 0) {
279
+ parts.push(qualified);
280
+ }
281
+ if (iri && iri.length > 0) {
282
+ const ns = iriNamespace(iri);
283
+ parts.push(ns ? `${iri}\nnamespace: ${ns}` : iri);
284
+ }
285
+ return parts.filter(Boolean).join('\n');
286
+ }
287
+
288
+ function toLiteralLabel(literal: any): string {
289
+ if (!literal) return '[literal]';
290
+ if (typeof literal.lexicalValue === 'string' && literal.lexicalValue.length > 0) {
291
+ return literal.lexicalValue;
292
+ }
293
+ if (typeof literal.value === 'string' && literal.value.length > 0) {
294
+ return literal.value;
295
+ }
296
+ if (typeof literal.stringValue === 'string' && literal.stringValue.length > 0) {
297
+ return literal.stringValue;
298
+ }
299
+ return '[literal]';
300
+ }
301
+
302
+ function getOneOfEntries(
303
+ statement: any,
304
+ ctx: DiagramBuildContext,
305
+ viewId: string,
306
+ astNodeLocator: { getAstNodePath(node: any): string } | undefined,
307
+ rootUri: string,
308
+ ): DiagramEntry[] {
309
+ const entries: DiagramEntry[] = [];
310
+ if (isLiteralEnumerationAxiom(statement)) {
311
+ let index = 0;
312
+ for (const literal of ((statement as any).literals ?? [])) {
313
+ const entryId = `${viewId}:oneOf:${index++}`;
314
+ entries.push({
315
+ id: entryId,
316
+ modelId: getModelIdForNode(statement, astNodeLocator, rootUri) ?? entryId,
317
+ text: toLiteralLabel(literal),
318
+ statement
319
+ });
320
+ }
321
+ return entries;
322
+ }
323
+ if (isInstanceEnumerationAxiom(statement)) {
324
+ let index = 0;
325
+ for (const instanceRef of ((statement as any).instances ?? [])) {
326
+ const qName = getRefDisplayName(ctx, instanceRef) ?? (instanceRef?.$refText ?? '[instance]');
327
+ const entryId = `${viewId}:oneOf:${index++}`;
328
+ entries.push({
329
+ id: entryId,
330
+ modelId: getModelIdForNode(statement, astNodeLocator, rootUri) ?? entryId,
331
+ text: toSimpleName(qName),
332
+ qualifiedText: qName,
333
+ statement
334
+ });
335
+ }
336
+ }
337
+ return entries;
338
+ }
339
+
340
+ function getScalarPropertyLabel(property: any, ctx: DiagramBuildContext): string | undefined {
341
+ const qName = getQualifiedName(ctx, property);
342
+ if (!qName) {
343
+ return undefined;
344
+ }
345
+ const ranges = ((property as any).ranges ?? [])
346
+ .map((rangeRef: any) => getRefDisplayName(ctx, rangeRef))
347
+ .filter((value: any): value is string => typeof value === 'string' && value.length > 0)
348
+ .map((value: string) => toSimpleName(value));
349
+ return ranges.length > 0 ? `${toSimpleName(qName)}: ${ranges.join(' | ')}` : toSimpleName(qName);
350
+ }
351
+
352
+ function getAssertionValueLabel(ctx: DiagramBuildContext, assertion: any): string {
353
+ const parts: string[] = [];
354
+ for (const literal of ((assertion as any).literalValues ?? [])) {
355
+ parts.push(toLiteralLabel(literal));
356
+ }
357
+ for (const referenced of ((assertion as any).referencedValues ?? [])) {
358
+ const qName = getRefDisplayName(ctx, referenced);
359
+ parts.push(qName ? toSimpleName(qName) : '[instance]');
360
+ }
361
+ for (const contained of ((assertion as any).containedValues ?? [])) {
362
+ const qName = getQualifiedName(ctx, contained);
363
+ parts.push(qName ? toSimpleName(qName) : '[instance]');
364
+ }
365
+ return parts.join(', ');
366
+ }
367
+
368
+ export async function computeLaidOutSModelForUri(shared: any, modelUri: string): Promise<SModelRoot> {
369
+ const documentUri = URI.parse(modelUri);
370
+ let document = shared.workspace.LangiumDocuments.getDocument(documentUri);
371
+ if (!document) {
372
+ return { id: 'root', type: 'graph', children: [] } as unknown as SModelRoot;
373
+ }
374
+ const root = document.parseResult?.value;
375
+ if (!root || !isOntology(root)) {
376
+ return { id: 'root', type: 'graph', children: [] } as unknown as SModelRoot;
377
+ }
378
+
379
+ const graph = await computeDiagramGraph(shared, document, root);
380
+ indexDiagramMappings(shared, graph);
381
+ return await layoutAndConvertToSModel(graph);
382
+ }
383
+
384
+ async function computeDiagramGraph(shared: any, document: LangiumDocument, root: Ontology): Promise<DiagramGraph> {
385
+ const graph: DiagramGraph = {
386
+ rootUri: document.uri.toString(),
387
+ root,
388
+ nodes: [],
389
+ edges: [],
390
+ statementsById: new Map<string, AstNode>()
391
+ };
392
+
393
+ if (isVocabulary(root)) {
394
+ computeVocabularyGraph(shared, root, graph);
395
+ return graph;
396
+ }
397
+
398
+ if (isDescription(root)) {
399
+ computeDescriptionGraph(shared, root, graph);
400
+ return graph;
401
+ }
402
+
403
+ if (isVocabularyBundle(root) || isDescriptionBundle(root)) {
404
+ await computeBundleGraph(shared, root, graph);
405
+ return graph;
406
+ }
407
+
408
+ return graph;
409
+ }
410
+
411
+ function computeVocabularyGraph(shared: any, root: any, graph: DiagramGraph): void {
412
+ const astNodeLocator = getAstNodeLocator(shared);
413
+ const ctx = buildDiagramContext(root);
414
+ const nodeByQName = new Map<string, DiagramNode>();
415
+ const statements = root.ownedStatements ?? [];
416
+
417
+ const ensureEntityNode = (candidate: any): DiagramNode | undefined => {
418
+ const isReferenceWrapper =
419
+ !!candidate
420
+ && typeof candidate === 'object'
421
+ && ('$refText' in candidate)
422
+ && ('ref' in candidate)
423
+ && !('$type' in candidate);
424
+ const statement = isReferenceWrapper ? candidate.ref : candidate;
425
+ const isConcreteEntity =
426
+ !!statement
427
+ && (isConcept(statement) || isAspect(statement) || isRelationEntity(statement) || isScalar(statement));
428
+ const qName = isConcreteEntity
429
+ ? getQualifiedName(ctx, statement)
430
+ : (isReferenceWrapper ? getRefDisplayName(ctx, candidate) : undefined);
431
+ if (!qName) return undefined;
432
+ const existing = nodeByQName.get(qName);
433
+ if (existing) return existing;
434
+
435
+ const viewId = `ent:${qName}`;
436
+ const modelId = isConcreteEntity
437
+ ? (getModelIdForNode(statement, astNodeLocator, graph.rootUri) ?? viewId)
438
+ : viewId;
439
+ const nodeKind: DiagramNode['kind'] = isConcreteEntity && isUnreifiedRelation(statement) ? 'relation' : 'entity';
440
+ const node: DiagramNode = {
441
+ id: viewId,
442
+ viewId,
443
+ modelId,
444
+ iri: isConcreteEntity ? getIriForNode(statement) : undefined,
445
+ label: toSimpleName(qName),
446
+ qualifiedLabel: qName,
447
+ tooltip: formatHoverLikeTooltip(
448
+ isConcreteEntity ? humanizeTypeName((statement as any).$type ?? 'entity') : 'entity',
449
+ qName,
450
+ isConcreteEntity ? getIriForNode(statement) : undefined
451
+ ),
452
+ propertyEntries: [],
453
+ compartmentEntries: isConcreteEntity
454
+ ? getOneOfEntries(statement, ctx, viewId, astNodeLocator, graph.rootUri)
455
+ : [],
456
+ kind: nodeKind,
457
+ statement: isConcreteEntity ? statement : undefined
458
+ };
459
+ graph.nodes.push(node);
460
+ if (isConcreteEntity) {
461
+ graph.statementsById.set(viewId, statement);
462
+ }
463
+ for (const entry of (node.compartmentEntries ?? [])) {
464
+ if (entry.statement) {
465
+ graph.statementsById.set(entry.id, entry.statement);
466
+ }
467
+ }
468
+ nodeByQName.set(qName, node);
469
+ return node;
470
+ };
471
+
472
+ for (const statement of statements) {
473
+ ensureEntityNode(statement);
474
+ }
475
+
476
+ for (const statement of statements) {
477
+ if (isScalarProperty(statement)) {
478
+ const propertyLabel = getScalarPropertyLabel(statement, ctx);
479
+ if (!propertyLabel) continue;
480
+ for (const domainRef of ((statement as any).domains ?? [])) {
481
+ const domainNode = ensureEntityNode(domainRef);
482
+ if (!domainNode) continue;
483
+ if (!domainNode.propertyEntries) domainNode.propertyEntries = [];
484
+ const entryId = `${domainNode.viewId}:properties:${domainNode.propertyEntries.length}`;
485
+ domainNode.propertyEntries.push({
486
+ id: entryId,
487
+ modelId: getModelIdForNode(statement, astNodeLocator, graph.rootUri) ?? entryId,
488
+ text: propertyLabel,
489
+ qualifiedText: propertyLabel,
490
+ tooltip: formatHoverLikeTooltip('scalar property', getQualifiedName(ctx, statement), getIriForNode(statement)),
491
+ statement
492
+ });
493
+ graph.statementsById.set(entryId, statement);
494
+ }
495
+ continue;
496
+ }
497
+
498
+ if (isRelationEntity(statement)) {
499
+ const relNode = ensureEntityNode(statement);
500
+ if (!relNode) continue;
501
+ const sources = (statement.sources ?? []).map((r: any) => ensureEntityNode(r)).filter(Boolean) as DiagramNode[];
502
+ const targets = (statement.targets ?? []).map((r: any) => ensureEntityNode(r)).filter(Boolean) as DiagramNode[];
503
+ let index = 0;
504
+ for (const src of sources) {
505
+ const edgeId = `relent:${relNode.id}:src:${index++}`;
506
+ const modelId = getModelIdForNode(statement, astNodeLocator, graph.rootUri) ?? edgeId;
507
+ graph.edges.push({
508
+ id: edgeId,
509
+ viewId: edgeId,
510
+ modelId,
511
+ iri: getIriForNode(statement),
512
+ sourceId: src.id,
513
+ targetId: relNode.id,
514
+ label: '',
515
+ kind: 'relation-source',
516
+ statement
517
+ });
518
+ graph.statementsById.set(edgeId, statement);
519
+ }
520
+ index = 0;
521
+ for (const dst of targets) {
522
+ const edgeId = `relent:${relNode.id}:dst:${index++}`;
523
+ const modelId = getModelIdForNode(statement, astNodeLocator, graph.rootUri) ?? edgeId;
524
+ graph.edges.push({
525
+ id: edgeId,
526
+ viewId: edgeId,
527
+ modelId,
528
+ iri: getIriForNode(statement),
529
+ sourceId: relNode.id,
530
+ targetId: dst.id,
531
+ label: '',
532
+ kind: 'relation-target',
533
+ statement
534
+ });
535
+ graph.statementsById.set(edgeId, statement);
536
+ }
537
+ continue;
538
+ }
539
+
540
+ if (!isUnreifiedRelation(statement)) continue;
541
+ const relationName = getQualifiedName(ctx, statement);
542
+ if (!relationName) continue;
543
+ const sources = (statement.sources ?? []).map((r: any) => ensureEntityNode(r)).filter(Boolean) as DiagramNode[];
544
+ const targets = (statement.targets ?? []).map((r: any) => ensureEntityNode(r)).filter(Boolean) as DiagramNode[];
545
+
546
+ if (sources.length === 1 && targets.length === 1) {
547
+ const edgeId = `urel:${relationName}`;
548
+ const modelId = getModelIdForNode(statement, astNodeLocator, graph.rootUri) ?? edgeId;
549
+ graph.edges.push({
550
+ id: edgeId,
551
+ viewId: edgeId,
552
+ modelId,
553
+ iri: getIriForNode(statement),
554
+ sourceId: sources[0].id,
555
+ targetId: targets[0].id,
556
+ label: toSimpleName(relationName),
557
+ qualifiedLabel: relationName,
558
+ tooltip: formatHoverLikeTooltip('relation', relationName, getIriForNode(statement)),
559
+ kind: 'relation',
560
+ statement
561
+ });
562
+ graph.statementsById.set(edgeId, statement);
563
+ continue;
564
+ }
565
+
566
+ const relationNodeViewId = `rel:${relationName}`;
567
+ let relationNode = nodeByQName.get(relationName);
568
+ if (!relationNode) {
569
+ const modelId = getModelIdForNode(statement, astNodeLocator, graph.rootUri) ?? relationNodeViewId;
570
+ relationNode = {
571
+ id: relationNodeViewId,
572
+ viewId: relationNodeViewId,
573
+ modelId,
574
+ iri: getIriForNode(statement),
575
+ label: toSimpleName(relationName),
576
+ qualifiedLabel: relationName,
577
+ tooltip: formatHoverLikeTooltip('relation', relationName, getIriForNode(statement)),
578
+ kind: 'relation',
579
+ statement
580
+ };
581
+ graph.nodes.push(relationNode);
582
+ graph.statementsById.set(relationNodeViewId, statement);
583
+ nodeByQName.set(relationName, relationNode);
584
+ }
585
+
586
+ let index = 0;
587
+ for (const src of sources) {
588
+ const edgeId = `urel:${relationName}:src:${index++}`;
589
+ const modelId = getModelIdForNode(statement, astNodeLocator, graph.rootUri) ?? edgeId;
590
+ graph.edges.push({
591
+ id: edgeId,
592
+ viewId: edgeId,
593
+ modelId,
594
+ iri: getIriForNode(statement),
595
+ sourceId: src.id,
596
+ targetId: relationNode.id,
597
+ label: '',
598
+ tooltip: formatHoverLikeTooltip('relation source', relationName, getIriForNode(statement)),
599
+ kind: 'relation-source',
600
+ statement
601
+ });
602
+ graph.statementsById.set(edgeId, statement);
603
+ }
604
+ index = 0;
605
+ for (const dst of targets) {
606
+ const edgeId = `urel:${relationName}:dst:${index++}`;
607
+ const modelId = getModelIdForNode(statement, astNodeLocator, graph.rootUri) ?? edgeId;
608
+ graph.edges.push({
609
+ id: edgeId,
610
+ viewId: edgeId,
611
+ modelId,
612
+ iri: getIriForNode(statement),
613
+ sourceId: relationNode.id,
614
+ targetId: dst.id,
615
+ label: '',
616
+ tooltip: formatHoverLikeTooltip('relation target', relationName, getIriForNode(statement)),
617
+ kind: 'relation-target',
618
+ statement
619
+ });
620
+ graph.statementsById.set(edgeId, statement);
621
+ }
622
+ }
623
+
624
+ const addSpecializationEdge = (owner: any, axiom: any, index: number): void => {
625
+ if (!isSpecializationAxiom(axiom)) return;
626
+ const superTerm = (axiom as any).superTerm?.ref;
627
+ const subNode = ensureEntityNode(owner);
628
+ const superNode = ensureEntityNode(superTerm);
629
+ if (!subNode || !superNode) return;
630
+ const edgeId = `spec:${subNode.id}:${superNode.id}:${index}`;
631
+ const modelId = getModelIdForNode(axiom, astNodeLocator, graph.rootUri) ?? edgeId;
632
+ graph.edges.push({
633
+ id: edgeId,
634
+ viewId: edgeId,
635
+ modelId,
636
+ iri: getIriForNode(axiom),
637
+ sourceId: subNode.id,
638
+ targetId: superNode.id,
639
+ label: '',
640
+ tooltip: formatHoverLikeTooltip('specialization', getQualifiedName(ctx, owner), getIriForNode(axiom)),
641
+ kind: 'specialization',
642
+ statement: axiom
643
+ });
644
+ graph.statementsById.set(edgeId, axiom);
645
+ };
646
+
647
+ const addEquivalenceEdges = (owner: any, axiom: any, index: number): void => {
648
+ if (!isEquivalenceAxiom(axiom)) return;
649
+ const subNode = ensureEntityNode(owner);
650
+ if (!subNode) return;
651
+ const superTerms = ((axiom as any).superTerms ?? []).map((ref: any) => ref?.ref).filter(Boolean);
652
+ if (superTerms.length === 1) {
653
+ const superNode = ensureEntityNode(superTerms[0]);
654
+ if (!superNode) return;
655
+ const edgeId = `eq:${subNode.id}:${superNode.id}:${index}:0`;
656
+ const modelId = getModelIdForNode(axiom, astNodeLocator, graph.rootUri) ?? edgeId;
657
+ graph.edges.push({
658
+ id: edgeId,
659
+ viewId: edgeId,
660
+ modelId,
661
+ iri: getIriForNode(axiom),
662
+ sourceId: subNode.id,
663
+ targetId: superNode.id,
664
+ label: '',
665
+ tooltip: formatHoverLikeTooltip('equivalence', getQualifiedName(ctx, owner), getIriForNode(axiom)),
666
+ kind: 'equivalence',
667
+ statement: axiom
668
+ });
669
+ graph.statementsById.set(edgeId, axiom);
670
+ return;
671
+ }
672
+
673
+ if (superTerms.length > 1) {
674
+ const eqNodeId = `eqnode:${subNode.id}:${index}`;
675
+ const eqNode: DiagramNode = {
676
+ id: eqNodeId,
677
+ viewId: eqNodeId,
678
+ modelId: getModelIdForNode(axiom, astNodeLocator, graph.rootUri) ?? eqNodeId,
679
+ iri: getIriForNode(axiom),
680
+ label: '&',
681
+ qualifiedLabel: getQualifiedName(ctx, owner),
682
+ tooltip: formatHoverLikeTooltip('equivalence', getQualifiedName(ctx, owner), getIriForNode(axiom)),
683
+ propertyEntries: [],
684
+ compartmentEntries: [],
685
+ kind: 'equivalence',
686
+ statement: axiom
687
+ };
688
+ graph.nodes.push(eqNode);
689
+ graph.statementsById.set(eqNodeId, axiom);
690
+
691
+ const inEdgeId = `eq:${subNode.id}:${eqNodeId}:${index}:in`;
692
+ graph.edges.push({
693
+ id: inEdgeId,
694
+ viewId: inEdgeId,
695
+ modelId: getModelIdForNode(axiom, astNodeLocator, graph.rootUri) ?? inEdgeId,
696
+ iri: getIriForNode(axiom),
697
+ sourceId: subNode.id,
698
+ targetId: eqNodeId,
699
+ label: '',
700
+ tooltip: formatHoverLikeTooltip('equivalence', getQualifiedName(ctx, owner), getIriForNode(axiom)),
701
+ kind: 'equivalence-source',
702
+ statement: axiom
703
+ });
704
+ graph.statementsById.set(inEdgeId, axiom);
705
+
706
+ let superIndex = 0;
707
+ for (const superTerm of superTerms) {
708
+ const superNode = ensureEntityNode(superTerm);
709
+ if (!superNode) continue;
710
+ const outEdgeId = `eq:${eqNodeId}:${superNode.id}:${index}:${superIndex++}`;
711
+ graph.edges.push({
712
+ id: outEdgeId,
713
+ viewId: outEdgeId,
714
+ modelId: getModelIdForNode(axiom, astNodeLocator, graph.rootUri) ?? outEdgeId,
715
+ iri: getIriForNode(axiom),
716
+ sourceId: eqNodeId,
717
+ targetId: superNode.id,
718
+ label: '',
719
+ tooltip: formatHoverLikeTooltip('equivalence', getQualifiedName(ctx, owner), getIriForNode(axiom)),
720
+ kind: 'equivalence',
721
+ statement: axiom
722
+ });
723
+ graph.statementsById.set(outEdgeId, axiom);
724
+ }
725
+ }
726
+ };
727
+
728
+ for (const statement of statements) {
729
+ const ownedSpecializations = (statement as any).ownedSpecializations ?? [];
730
+ for (let i = 0; i < ownedSpecializations.length; i += 1) {
731
+ addSpecializationEdge(statement, ownedSpecializations[i], i);
732
+ }
733
+ const ownedEquivalences = (statement as any).ownedEquivalences ?? [];
734
+ for (let i = 0; i < ownedEquivalences.length; i += 1) {
735
+ addEquivalenceEdges(statement, ownedEquivalences[i], i);
736
+ }
737
+ }
738
+ }
739
+
740
+ function computeDescriptionGraph(shared: any, root: any, graph: DiagramGraph): void {
741
+ const astNodeLocator = getAstNodeLocator(shared);
742
+ const ctx = buildDiagramContext(root);
743
+ const nodeByQName = new Map<string, DiagramNode>();
744
+
745
+ const ensureInstanceNode = (candidate: any): DiagramNode | undefined => {
746
+ const statement = unwrapReferenceValue(candidate);
747
+ if (!statement || (!isConceptInstance(statement) && !isRelationInstance(statement))) return undefined;
748
+ const qName = getQualifiedName(ctx, statement);
749
+ if (!qName) return undefined;
750
+ const existing = nodeByQName.get(qName);
751
+ if (existing) return existing;
752
+ const viewId = `ci:${qName}`;
753
+ const modelId = getModelIdForNode(statement, astNodeLocator, graph.rootUri) ?? viewId;
754
+ const typeLabels = Array.from(new Set(
755
+ ((statement as any).ownedTypes ?? [])
756
+ .map((typeAssertion: any) => getQualifiedName(ctx, typeAssertion?.type?.ref ?? typeAssertion?.type))
757
+ .filter((label: any) => typeof label === 'string' && label.length > 0)
758
+ )).map((label) => toSimpleName(label as string | undefined)) as string[];
759
+ const label = toSimpleName(qName);
760
+ const propertyEntries: DiagramEntry[] = [];
761
+ let propertyIndex = 0;
762
+ for (const assertion of (((statement as any).ownedPropertyValues ?? []) as any[])) {
763
+ if (getContainingOntology(assertion) !== root) continue;
764
+ if (!isPropertyValueAssertion(assertion)) continue;
765
+ const propertyRef = (assertion as any).property?.ref;
766
+ if (!propertyRef || !isScalarProperty(propertyRef)) continue;
767
+ const propertyName = getQualifiedName(ctx, propertyRef) ?? (assertion as any).property?.$refText ?? 'property';
768
+ const valueLabel = getAssertionValueLabel(ctx, assertion);
769
+ const text = valueLabel.length > 0
770
+ ? `${toSimpleName(propertyName)} = ${valueLabel}`
771
+ : toSimpleName(propertyName);
772
+ const entryId = `${viewId}:properties:${propertyIndex++}`;
773
+ propertyEntries.push({
774
+ id: entryId,
775
+ modelId: getModelIdForNode(assertion, astNodeLocator, graph.rootUri) ?? entryId,
776
+ text,
777
+ qualifiedText: propertyName,
778
+ statement: assertion
779
+ });
780
+ }
781
+ const node: DiagramNode = {
782
+ id: viewId,
783
+ viewId,
784
+ modelId,
785
+ iri: getIriForNode(statement),
786
+ label,
787
+ instanceTypeLabels: typeLabels,
788
+ qualifiedLabel: qName,
789
+ tooltip: formatHoverLikeTooltip(humanizeTypeName((statement as any).$type ?? 'instance'), qName, getIriForNode(statement)),
790
+ propertyEntries,
791
+ kind: 'instance',
792
+ statement
793
+ };
794
+ graph.nodes.push(node);
795
+ graph.statementsById.set(viewId, statement);
796
+ for (const entry of propertyEntries) {
797
+ if (entry.statement) {
798
+ graph.statementsById.set(entry.id, entry.statement);
799
+ }
800
+ }
801
+ nodeByQName.set(qName, node);
802
+ return node;
803
+ };
804
+
805
+ const statements = root.ownedStatements ?? [];
806
+ for (const statement of statements) {
807
+ ensureInstanceNode(statement);
808
+ }
809
+
810
+ let assertionEdgeIndex = 0;
811
+ const addRelationPropertyAssertionEdges = (ownerStatement: any, ownerNode: DiagramNode | undefined): void => {
812
+ if (!ownerNode) return;
813
+ const ownedPropertyValues = (ownerStatement as any).ownedPropertyValues ?? [];
814
+ for (const assertion of ownedPropertyValues) {
815
+ if (!isPropertyValueAssertion(assertion)) continue;
816
+ const propertyRef = (assertion as any).property?.ref;
817
+ if (!propertyRef || !isRelation(propertyRef)) continue;
818
+ const relationName = getQualifiedName(ctx, propertyRef) ?? (assertion as any).property?.$refText ?? 'relation';
819
+ const referencedValues = (assertion as any).referencedValues ?? [];
820
+ for (const valueRef of referencedValues) {
821
+ const targetNode = ensureInstanceNode(valueRef);
822
+ if (!targetNode) continue;
823
+ const edgeId = `pva:${ownerNode.id}:${targetNode.id}:${assertionEdgeIndex++}`;
824
+ const modelId = getModelIdForNode(assertion, astNodeLocator, graph.rootUri) ?? edgeId;
825
+ graph.edges.push({
826
+ id: edgeId,
827
+ viewId: edgeId,
828
+ modelId,
829
+ iri: getIriForNode(propertyRef),
830
+ sourceId: ownerNode.id,
831
+ targetId: targetNode.id,
832
+ label: toSimpleName(relationName),
833
+ qualifiedLabel: relationName,
834
+ tooltip: formatHoverLikeTooltip('relation', relationName, getIriForNode(propertyRef)),
835
+ kind: 'relation',
836
+ statement: assertion
837
+ });
838
+ graph.statementsById.set(edgeId, assertion);
839
+ }
840
+ }
841
+ };
842
+
843
+ for (const statement of statements) {
844
+ const ownerNode = ensureInstanceNode(statement);
845
+ addRelationPropertyAssertionEdges(statement, ownerNode);
846
+
847
+ if (!isRelationInstance(statement)) continue;
848
+ if (!ownerNode) continue;
849
+ const relName = getQualifiedName(ctx, statement) ?? statement.name ?? 'relation';
850
+ const sources = (statement.sources ?? []).map((r: any) => ensureInstanceNode(r)).filter(Boolean) as DiagramNode[];
851
+ const targets = (statement.targets ?? []).map((r: any) => ensureInstanceNode(r)).filter(Boolean) as DiagramNode[];
852
+ let index = 0;
853
+ for (const src of sources) {
854
+ const edgeId = `ri:${relName}:src:${index++}`;
855
+ const modelId = getModelIdForNode(statement, astNodeLocator, graph.rootUri) ?? edgeId;
856
+ graph.edges.push({
857
+ id: edgeId,
858
+ viewId: edgeId,
859
+ modelId,
860
+ iri: getIriForNode(statement),
861
+ sourceId: src.id,
862
+ targetId: ownerNode.id,
863
+ label: '',
864
+ tooltip: formatHoverLikeTooltip('relation source', relName, getIriForNode(statement)),
865
+ kind: 'relation-source',
866
+ statement
867
+ });
868
+ graph.statementsById.set(edgeId, statement);
869
+ }
870
+ index = 0;
871
+ for (const dst of targets) {
872
+ const edgeId = `ri:${relName}:dst:${index++}`;
873
+ const modelId = getModelIdForNode(statement, astNodeLocator, graph.rootUri) ?? edgeId;
874
+ graph.edges.push({
875
+ id: edgeId,
876
+ viewId: edgeId,
877
+ modelId,
878
+ iri: getIriForNode(statement),
879
+ sourceId: ownerNode.id,
880
+ targetId: dst.id,
881
+ label: '',
882
+ tooltip: formatHoverLikeTooltip('relation target', relName, getIriForNode(statement)),
883
+ kind: 'relation-target',
884
+ statement
885
+ });
886
+ graph.statementsById.set(edgeId, statement);
887
+ }
888
+ }
889
+ }
890
+
891
+ async function computeBundleGraph(shared: any, root: Ontology, graph: DiagramGraph): Promise<void> {
892
+ const astNodeLocator = getAstNodeLocator(shared);
893
+ const queue: Ontology[] = [root];
894
+ const seen = new Set<string>();
895
+ const nodeByNamespace = new Map<string, DiagramNode>();
896
+ const directImports = new Map<string, Array<{ targetNamespace: string; ownedImport: Import }>>();
897
+
898
+ const seedImports = ((root as any).ownedImports ?? []) as Import[];
899
+ for (const ownedImport of seedImports) {
900
+ const importedOntology = await resolveImportedOntology(shared, ownedImport);
901
+ if (!importedOntology) continue;
902
+ queue.push(importedOntology);
903
+ }
904
+ const ensureOntologyNode = (ontology: Ontology, namespace: string): DiagramNode => {
905
+ const existing = nodeByNamespace.get(namespace);
906
+ if (existing) return existing;
907
+ const nodeId = `ont:${namespace}`;
908
+ const modelId = namespace || getModelIdForNode(ontology, astNodeLocator, graph.rootUri) || nodeId;
909
+ const label = (ontology as any).prefix || namespace.split('/').pop() || namespace;
910
+ const node: DiagramNode = {
911
+ id: nodeId,
912
+ viewId: nodeId,
913
+ modelId,
914
+ iri: getIriForNode(ontology),
915
+ label,
916
+ kind: 'ontology',
917
+ statement: ontology
918
+ };
919
+ node.tooltip = getIriForNode(ontology);
920
+ graph.nodes.push(node);
921
+ graph.statementsById.set(nodeId, ontology);
922
+ nodeByNamespace.set(namespace, node);
923
+ return node;
924
+ };
925
+
926
+ while (queue.length > 0) {
927
+ const current = queue.shift()!;
928
+ const namespace = normalizeNamespace((current as any).namespace ?? '');
929
+ if (!namespace || seen.has(namespace)) continue;
930
+ seen.add(namespace);
931
+
932
+ ensureOntologyNode(current, namespace);
933
+
934
+ for (const ownedImport of ((current as any).ownedImports ?? []) as Import[]) {
935
+ const importedOntology = await resolveImportedOntology(shared, ownedImport);
936
+ if (!importedOntology) continue;
937
+ const importedNamespace = normalizeNamespace((importedOntology as any).namespace ?? '');
938
+ if (!importedNamespace) continue;
939
+
940
+ if (!seen.has(importedNamespace)) {
941
+ queue.push(importedOntology);
942
+ }
943
+
944
+ ensureOntologyNode(importedOntology, importedNamespace);
945
+ const existing = directImports.get(namespace) ?? [];
946
+ existing.push({ targetNamespace: importedNamespace, ownedImport });
947
+ directImports.set(namespace, existing);
948
+ }
949
+ }
950
+
951
+ const reachabilityWithoutDirectEdge = (source: string, target: string): boolean => {
952
+ const visited = new Set<string>([source]);
953
+ const work: string[] = [];
954
+ const firstHops = directImports.get(source) ?? [];
955
+ for (const hop of firstHops) {
956
+ if (hop.targetNamespace === target) continue;
957
+ work.push(hop.targetNamespace);
958
+ }
959
+ while (work.length > 0) {
960
+ const current = work.shift()!;
961
+ if (current === target) return true;
962
+ if (visited.has(current)) continue;
963
+ visited.add(current);
964
+ for (const next of (directImports.get(current) ?? [])) {
965
+ if (!visited.has(next.targetNamespace)) {
966
+ work.push(next.targetNamespace);
967
+ }
968
+ }
969
+ }
970
+ return false;
971
+ };
972
+
973
+ for (const [sourceNamespace, imports] of directImports.entries()) {
974
+ const sourceNode = nodeByNamespace.get(sourceNamespace);
975
+ if (!sourceNode) continue;
976
+ for (const imported of imports) {
977
+ const targetNode = nodeByNamespace.get(imported.targetNamespace);
978
+ if (!targetNode) continue;
979
+ if (reachabilityWithoutDirectEdge(sourceNamespace, imported.targetNamespace)) {
980
+ continue;
981
+ }
982
+ const edgeId = `imp:${sourceNamespace}->${imported.targetNamespace}`;
983
+ const edgeModelId = getModelIdForNode(imported.ownedImport, astNodeLocator, graph.rootUri) ?? edgeId;
984
+ graph.edges.push({
985
+ id: edgeId,
986
+ viewId: edgeId,
987
+ modelId: edgeModelId,
988
+ iri: getIriForNode(imported.ownedImport),
989
+ sourceId: sourceNode.id,
990
+ targetId: targetNode.id,
991
+ label: '',
992
+ tooltip: getIriForNode(imported.ownedImport),
993
+ kind: 'import',
994
+ statement: imported.ownedImport
995
+ });
996
+ graph.statementsById.set(edgeId, imported.ownedImport);
997
+ }
998
+ }
999
+ }
1000
+
1001
+ async function resolveImportedOntology(shared: any, ownedImport: Import): Promise<Ontology | undefined> {
1002
+ const refText = (ownedImport as any)?.imported?.$refText ?? '';
1003
+ const namespace = normalizeNamespace(String(refText).replace(/^<|>$/g, ''));
1004
+ if (namespace) {
1005
+ const resolved = await findOntologyByNamespace(shared, namespace);
1006
+ if (resolved) {
1007
+ return resolved;
1008
+ }
1009
+ }
1010
+ const direct = (ownedImport as any)?.imported?.ref;
1011
+ if (direct && isOntology(direct)) {
1012
+ return direct;
1013
+ }
1014
+ return undefined;
1015
+ }
1016
+
1017
+ function getNodeSize(node: DiagramNode): { width: number; height: number } {
1018
+ if (node.kind === 'equivalence') {
1019
+ return { width: 28, height: 28 };
1020
+ }
1021
+ const properties = node.propertyEntries ?? [];
1022
+ const entries = node.compartmentEntries ?? [];
1023
+ const maxHeaderChars = 34;
1024
+ const maxBodyChars = 38;
1025
+ const headerName = Math.min(node.label.length, maxHeaderChars);
1026
+ const headerType = Math.min(getNodeTypeLabel(node).length, maxHeaderChars);
1027
+ const widestHeaderPx = Math.max(
1028
+ Math.round(headerName * 8.2),
1029
+ Math.round(headerType * 6.9)
1030
+ );
1031
+ const widestBodyPx = Math.max(
1032
+ ...[...properties, ...entries].map((entry) => Math.round(Math.min(entry.text.length, maxBodyChars) * 6.0)),
1033
+ 0
1034
+ );
1035
+ const contentWidth = Math.max(widestHeaderPx, widestBodyPx);
1036
+ const width = Math.max(120, Math.min(340, contentWidth + 28));
1037
+ const headerHeight = 34;
1038
+ const compartmentPadding = 10;
1039
+ const entryHeight = 16;
1040
+ const propertyHeight = properties.length > 0
1041
+ ? (compartmentPadding * 2) + (properties.length * entryHeight)
1042
+ : 0;
1043
+ const oneOfHeight = entries.length > 0
1044
+ ? (compartmentPadding * 2) + (entries.length * entryHeight)
1045
+ : 0;
1046
+ const gap = properties.length > 0 && entries.length > 0 ? 4 : 0;
1047
+ const bodyHeight = propertyHeight + gap + oneOfHeight + (propertyHeight + oneOfHeight > 0 ? 0 : 22);
1048
+ return { width, height: headerHeight + bodyHeight };
1049
+ }
1050
+
1051
+ function getNodeTypeLabel(node: DiagramNode): string {
1052
+ if (node.kind === 'equivalence') return '«equivalence»';
1053
+ if (node.kind === 'instance') {
1054
+ const types = (node.instanceTypeLabels ?? []).filter((t) => t.length > 0);
1055
+ if (types.length > 0) {
1056
+ return `«${types.join(', ')}»`;
1057
+ }
1058
+ return '«concept instance»';
1059
+ }
1060
+ const explicitType = (node.statement as any)?.$type;
1061
+ if (typeof explicitType === 'string' && explicitType.length > 0) {
1062
+ return `«${humanizeTypeName(explicitType)}»`;
1063
+ }
1064
+ if (node.kind === 'ontology') return '«ontology»';
1065
+ return '«concept»';
1066
+ }
1067
+
1068
+ async function layoutAndConvertToSModel(graph: DiagramGraph): Promise<SModelRoot> {
1069
+ if (graph.nodes.length === 0) {
1070
+ return { id: 'root', type: 'graph', children: [] } as unknown as SModelRoot;
1071
+ }
1072
+
1073
+ const selfLoopCountByNode = new Map<string, number>();
1074
+ for (const edge of graph.edges) {
1075
+ if (edge.sourceId === edge.targetId) {
1076
+ const current = selfLoopCountByNode.get(edge.sourceId) ?? 0;
1077
+ selfLoopCountByNode.set(edge.sourceId, current + 1);
1078
+ }
1079
+ }
1080
+ const layoutNodeSizeById = new Map<string, { width: number; height: number }>();
1081
+ for (const node of graph.nodes) {
1082
+ const baseSize = getNodeSize(node);
1083
+ const selfLoopCount = selfLoopCountByNode.get(node.id) ?? 0;
1084
+ if (selfLoopCount === 0) {
1085
+ layoutNodeSizeById.set(node.id, baseSize);
1086
+ continue;
1087
+ }
1088
+ const labelHeight = 16;
1089
+ const loopSpacing = labelHeight + 8;
1090
+ const baseLoopHeight = 40;
1091
+ const outermostLoopIndex = selfLoopCount - 1;
1092
+ const outermostLoopHeight = baseLoopHeight + (outermostLoopIndex * loopSpacing);
1093
+ const minLoopHostHeight = outermostLoopHeight + 20;
1094
+ layoutNodeSizeById.set(node.id, {
1095
+ width: baseSize.width,
1096
+ height: Math.max(baseSize.height, minLoopHostHeight)
1097
+ });
1098
+ }
1099
+
1100
+ const elk = await getElk();
1101
+
1102
+ const elkGraph: any = {
1103
+ id: 'root',
1104
+ layoutOptions: {
1105
+ 'elk.algorithm': 'org.eclipse.elk.layered',
1106
+ 'elk.direction': 'DOWN',
1107
+ 'elk.layered.nodePlacement.strategy': 'BRANDES_KOEPF',
1108
+ 'elk.spacing.nodeNode': '50',
1109
+ 'elk.layered.spacing.nodeNodeBetweenLayers': '80',
1110
+ 'elk.selfLoopDistribution': 'EQUALLY_SPACED',
1111
+ 'elk.selfLoopOrdering': 'STACKED',
1112
+ 'elk.spacing.edgeSelfLoop': '72',
1113
+ 'elk.spacing.edgeEdge': '20',
1114
+ 'elk.spacing.edgeNode': '20',
1115
+ 'elk.edgeRouting': 'ORTHOGONAL',
1116
+ 'elk.layered.crossingMinimization.strategy': 'LAYER_SWEEP',
1117
+ 'elk.layered.unnecessaryBendpoints': 'true',
1118
+ 'elk.layered.nodePlacement.bk.fixedAlignment': 'BALANCED',
1119
+ 'elk.layered.considerModelOrder.strategy': 'NODES_AND_EDGES',
1120
+ 'elk.hierarchyHandling': 'INCLUDE_CHILDREN'
1121
+ },
1122
+ children: graph.nodes.map((n) => {
1123
+ const size = layoutNodeSizeById.get(n.id) ?? getNodeSize(n);
1124
+ return { id: n.id, width: size.width, height: size.height };
1125
+ }),
1126
+ edges: graph.edges.map((e) => ({
1127
+ id: e.id,
1128
+ sources: [e.sourceId],
1129
+ targets: [e.targetId],
1130
+ labels: e.label
1131
+ ? [{
1132
+ id: `${e.id}:elk-label`,
1133
+ text: e.label,
1134
+ width: e.label.length * 7 + 8,
1135
+ height: 14,
1136
+ layoutOptions: {
1137
+ 'elk.edgeLabels.inline': 'false',
1138
+ 'elk.edgeLabels.placement': e.sourceId === e.targetId ? 'TAIL' : 'CENTER'
1139
+ }
1140
+ }]
1141
+ : []
1142
+ }))
1143
+ };
1144
+
1145
+ const laidOut = await elk.layout(elkGraph);
1146
+ const nodeMap = new Map<string, any>((laidOut.children ?? []).map((n: any) => [n.id, n]));
1147
+ const edgeMap = new Map<string, any>((laidOut.edges ?? []).map((e: any) => [e.id, e]));
1148
+ const diagramNodeById = new Map<string, DiagramNode>(graph.nodes.map((node) => [node.id, node]));
1149
+
1150
+ const extractRoutingPoints = (elkEdge: any): Array<{ x: number; y: number }> => {
1151
+ if (!elkEdge || !Array.isArray(elkEdge.sections) || elkEdge.sections.length === 0) return [];
1152
+ const section = elkEdge.sections[0];
1153
+ const points: Array<{ x: number; y: number }> = [];
1154
+ const pushPoint = (p: any): void => {
1155
+ if (!p || typeof p.x !== 'number' || typeof p.y !== 'number') return;
1156
+ const x = Number(p.x);
1157
+ const y = Number(p.y);
1158
+ if (!Number.isFinite(x) || !Number.isFinite(y)) return;
1159
+ const last = points[points.length - 1];
1160
+ if (!last || Math.abs(last.x - x) > 0.001 || Math.abs(last.y - y) > 0.001) {
1161
+ points.push({ x, y });
1162
+ }
1163
+ };
1164
+ pushPoint(section.startPoint);
1165
+ for (const bend of Array.isArray(section.bendPoints) ? section.bendPoints : []) {
1166
+ pushPoint(bend);
1167
+ }
1168
+ pushPoint(section.endPoint);
1169
+ return simplifyRoutingPoints(points);
1170
+ };
1171
+
1172
+ const simplifyRoutingPoints = (points: Array<{ x: number; y: number }>): Array<{ x: number; y: number }> => {
1173
+ if (points.length <= 2) return points;
1174
+ const simplified: Array<{ x: number; y: number }> = [points[0]];
1175
+ for (let i = 1; i < points.length - 1; i++) {
1176
+ const prev = points[i - 1];
1177
+ const curr = points[i];
1178
+ const next = points[i + 1];
1179
+ const isHorizontalLine = Math.abs(prev.y - curr.y) < 0.001 && Math.abs(curr.y - next.y) < 0.001;
1180
+ const isVerticalLine = Math.abs(prev.x - curr.x) < 0.001 && Math.abs(curr.x - next.x) < 0.001;
1181
+ if (!isHorizontalLine && !isVerticalLine) {
1182
+ simplified.push(curr);
1183
+ }
1184
+ }
1185
+ simplified.push(points[points.length - 1]);
1186
+ return simplified;
1187
+ };
1188
+
1189
+ const rectangularSelfLoopRoutingPoints = (
1190
+ nodeLayout: { x: number; y: number; width: number; height: number },
1191
+ loopIndex: number
1192
+ ): Array<{ x: number; y: number }> => {
1193
+ const leftX = nodeLayout.x;
1194
+ const centerY = nodeLayout.y + (nodeLayout.height / 2);
1195
+ const labelHeight = 16;
1196
+ const loopSpacing = labelHeight + 8;
1197
+ const baseLoopWidth = 60;
1198
+ const baseLoopHeight = 40;
1199
+ const loopWidth = baseLoopWidth + (loopIndex * loopSpacing);
1200
+ const loopHeight = baseLoopHeight + (loopIndex * loopSpacing);
1201
+ const top = centerY - (loopHeight / 2);
1202
+ const bottom = centerY + (loopHeight / 2);
1203
+ const outerX = leftX - loopWidth;
1204
+ return [
1205
+ { x: leftX, y: top },
1206
+ { x: outerX, y: top },
1207
+ { x: outerX, y: bottom },
1208
+ { x: leftX, y: bottom }
1209
+ ];
1210
+ };
1211
+
1212
+ const selfLoopOrdinalByNode = new Map<string, number>();
1213
+
1214
+ const children: SModelElement[] = [];
1215
+ const headerHeight = 34;
1216
+ const entryHeight = 16;
1217
+ const compartmentPadding = 10;
1218
+ for (const node of graph.nodes) {
1219
+ const laid = nodeMap.get(node.viewId);
1220
+ const size = getNodeSize(node);
1221
+ const width = laid?.width ?? size.width;
1222
+ const height = laid?.height ?? size.height;
1223
+ if (node.kind === 'equivalence') {
1224
+ children.push({
1225
+ id: node.viewId,
1226
+ viewId: node.viewId,
1227
+ modelId: node.modelId,
1228
+ tooltip: node.iri,
1229
+ type: 'node:rect',
1230
+ position: { x: laid?.x ?? 0, y: laid?.y ?? 0 },
1231
+ size: { width, height },
1232
+ kind: node.kind,
1233
+ children: [
1234
+ {
1235
+ id: `${node.viewId}:name`,
1236
+ viewId: `${node.viewId}:name`,
1237
+ modelId: node.modelId,
1238
+ tooltip: node.iri,
1239
+ type: 'label',
1240
+ text: '&',
1241
+ position: { x: width / 2, y: height / 2 }
1242
+ } as unknown as SModelElement
1243
+ ]
1244
+ } as unknown as SModelElement);
1245
+ continue;
1246
+ }
1247
+ const propertyEntries = node.propertyEntries ?? [];
1248
+ const entries = node.compartmentEntries ?? [];
1249
+ const maxBodyChars = Math.max(8, Math.floor((width - 24) / 6.0));
1250
+ const propertyHeight = propertyEntries.length > 0
1251
+ ? (compartmentPadding * 2) + (propertyEntries.length * entryHeight)
1252
+ : 0;
1253
+ const oneOfHeight = entries.length > 0
1254
+ ? (compartmentPadding * 2) + (entries.length * entryHeight)
1255
+ : 0;
1256
+ const gap = propertyEntries.length > 0 && entries.length > 0 ? 4 : 0;
1257
+ const nodeChildren: SModelElement[] = [
1258
+ {
1259
+ id: `${node.viewId}:header`,
1260
+ viewId: `${node.viewId}:header`,
1261
+ modelId: node.modelId,
1262
+ tooltip: node.iri,
1263
+ type: 'node:rect',
1264
+ position: { x: 0, y: 0 },
1265
+ size: { width, height: headerHeight },
1266
+ kind: 'label-box',
1267
+ children: [
1268
+ {
1269
+ id: `${node.viewId}:type`,
1270
+ viewId: `${node.viewId}:type`,
1271
+ modelId: node.modelId,
1272
+ tooltip: node.iri,
1273
+ type: 'label',
1274
+ text: getNodeTypeLabel(node),
1275
+ position: { x: width / 2, y: 10 }
1276
+ },
1277
+ {
1278
+ id: `${node.viewId}:name`,
1279
+ viewId: `${node.viewId}:name`,
1280
+ modelId: node.modelId,
1281
+ tooltip: node.iri,
1282
+ type: 'label',
1283
+ text: node.label,
1284
+ position: { x: width / 2, y: 24 }
1285
+ }
1286
+ ]
1287
+ } as unknown as SModelElement
1288
+ ];
1289
+
1290
+ let compartmentY = headerHeight;
1291
+ if (propertyEntries.length > 0) {
1292
+ nodeChildren.push({
1293
+ id: `${node.viewId}:properties`,
1294
+ viewId: `${node.viewId}:properties`,
1295
+ modelId: node.modelId,
1296
+ tooltip: node.iri,
1297
+ type: 'node:rect',
1298
+ position: { x: 0, y: compartmentY },
1299
+ size: { width, height: propertyHeight },
1300
+ kind: 'property-compartment',
1301
+ children: propertyEntries.map((entry, index) => ({
1302
+ id: entry.id,
1303
+ viewId: entry.id,
1304
+ modelId: entry.modelId,
1305
+ tooltip: getIriForNode(entry.statement) ?? node.iri,
1306
+ type: 'label',
1307
+ text: truncateWithEllipsis(entry.text, maxBodyChars),
1308
+ position: {
1309
+ x: 12,
1310
+ y: compartmentPadding + 8 + (index * entryHeight)
1311
+ }
1312
+ })) as unknown as SModelElement[]
1313
+ } as unknown as SModelElement);
1314
+ compartmentY += propertyHeight + gap;
1315
+ }
1316
+
1317
+ if (entries.length > 0) {
1318
+ nodeChildren.push({
1319
+ id: `${node.viewId}:compartment`,
1320
+ viewId: `${node.viewId}:compartment`,
1321
+ modelId: node.modelId,
1322
+ tooltip: node.iri,
1323
+ type: 'node:rect',
1324
+ position: { x: 0, y: compartmentY },
1325
+ size: { width, height: oneOfHeight },
1326
+ kind: 'compartment',
1327
+ children: entries.map((entry, index) => ({
1328
+ id: entry.id,
1329
+ viewId: entry.id,
1330
+ modelId: entry.modelId,
1331
+ tooltip: getIriForNode(entry.statement) ?? node.iri,
1332
+ type: 'label',
1333
+ text: truncateWithEllipsis(entry.text, maxBodyChars),
1334
+ position: {
1335
+ x: 12,
1336
+ y: compartmentPadding + 8 + (index * entryHeight)
1337
+ }
1338
+ })) as unknown as SModelElement[]
1339
+ } as unknown as SModelElement);
1340
+ }
1341
+
1342
+ children.push({
1343
+ id: node.viewId,
1344
+ viewId: node.viewId,
1345
+ modelId: node.modelId,
1346
+ tooltip: node.iri,
1347
+ type: 'node:rect',
1348
+ position: { x: laid?.x ?? 0, y: laid?.y ?? 0 },
1349
+ size: { width, height },
1350
+ kind: node.kind,
1351
+ children: nodeChildren
1352
+ } as unknown as SModelElement);
1353
+ }
1354
+
1355
+ for (const edge of graph.edges) {
1356
+ const laidEdge = edgeMap.get(edge.viewId);
1357
+ const isSelfLoop = edge.sourceId === edge.targetId;
1358
+ const selfLoopIndex = isSelfLoop
1359
+ ? (selfLoopOrdinalByNode.get(edge.sourceId) ?? 0)
1360
+ : -1;
1361
+ if (isSelfLoop) {
1362
+ selfLoopOrdinalByNode.set(edge.sourceId, selfLoopIndex + 1);
1363
+ }
1364
+ const routingPoints = (() => {
1365
+ if (!isSelfLoop) return extractRoutingPoints(laidEdge);
1366
+ const laidNode = nodeMap.get(edge.sourceId);
1367
+ const fallbackNode = diagramNodeById.get(edge.sourceId);
1368
+ const fallbackSize = fallbackNode ? getNodeSize(fallbackNode) : { width: 120, height: 56 };
1369
+ const nodeLayout = {
1370
+ x: laidNode?.x ?? 0,
1371
+ y: laidNode?.y ?? 0,
1372
+ width: laidNode?.width ?? fallbackSize.width,
1373
+ height: laidNode?.height ?? fallbackSize.height
1374
+ };
1375
+ return rectangularSelfLoopRoutingPoints(nodeLayout, selfLoopIndex);
1376
+ })();
1377
+ const edgeChild = edge.label
1378
+ ? [(() => {
1379
+ if (isSelfLoop) {
1380
+ return {
1381
+ id: `${edge.viewId}:label`,
1382
+ viewId: `${edge.viewId}:label`,
1383
+ modelId: edge.modelId,
1384
+ tooltip: edge.iri,
1385
+ type: 'label:edge',
1386
+ text: edge.label,
1387
+ edgePlacement: {
1388
+ rotate: false,
1389
+ side: 'top',
1390
+ position: 0.75,
1391
+ offset: 8,
1392
+ moveMode: 'edge'
1393
+ }
1394
+ };
1395
+ }
1396
+ return {
1397
+ id: `${edge.viewId}:label`,
1398
+ viewId: `${edge.viewId}:label`,
1399
+ modelId: edge.modelId,
1400
+ tooltip: edge.iri,
1401
+ type: 'label:edge',
1402
+ text: edge.label,
1403
+ edgePlacement: {
1404
+ rotate: false,
1405
+ side: 'top',
1406
+ position: 0.5,
1407
+ offset: 8,
1408
+ moveMode: 'edge'
1409
+ }
1410
+ };
1411
+ })()]
1412
+ : [];
1413
+ children.push({
1414
+ id: edge.viewId,
1415
+ viewId: edge.viewId,
1416
+ modelId: edge.modelId,
1417
+ tooltip: edge.iri,
1418
+ type: `edge:${edge.kind}`,
1419
+ sourceId: edge.sourceId,
1420
+ targetId: edge.targetId,
1421
+ routerKind: 'polyline',
1422
+ routingPoints,
1423
+ kind: edge.kind,
1424
+ children: edgeChild
1425
+ } as unknown as SModelElement);
1426
+ }
1427
+
1428
+ return {
1429
+ id: 'root',
1430
+ type: 'graph',
1431
+ children
1432
+ } as unknown as SModelRoot;
1433
+ }
1434
+
1435
+ function normalizeDiagramElementId(elementId: string): string {
1436
+ return elementId.endsWith(':label') ? elementId.slice(0, -':label'.length) : elementId;
1437
+ }
1438
+
1439
+ function indexDiagramMappings(shared: any, graph: DiagramGraph): void {
1440
+ const astNodeLocator = getAstNodeLocator(shared);
1441
+ if (!astNodeLocator) {
1442
+ navigationIndex.set(graph.rootUri, new Map());
1443
+ return;
1444
+ }
1445
+ const map = new Map<string, AstRef>();
1446
+ for (const node of graph.nodes) {
1447
+ const statement = node.statement;
1448
+ if (!statement) continue;
1449
+ const root = statement.$document?.parseResult?.value;
1450
+ if (!root) continue;
1451
+ const path = astNodeLocator.getAstNodePath(statement);
1452
+ if (!path) continue;
1453
+ const uri = statement.$document?.uri?.toString() ?? graph.rootUri;
1454
+ const astRef = { uri, path };
1455
+ map.set(normalizeDiagramElementId(node.viewId), astRef);
1456
+ map.set(normalizeDiagramElementId(node.modelId), astRef);
1457
+ }
1458
+ for (const edge of graph.edges) {
1459
+ const statement = edge.statement;
1460
+ if (!statement) continue;
1461
+ const root = statement.$document?.parseResult?.value;
1462
+ if (!root) continue;
1463
+ const path = astNodeLocator.getAstNodePath(statement);
1464
+ if (!path) continue;
1465
+ const uri = statement.$document?.uri?.toString() ?? graph.rootUri;
1466
+ const astRef = { uri, path };
1467
+ map.set(normalizeDiagramElementId(edge.viewId), astRef);
1468
+ map.set(normalizeDiagramElementId(edge.modelId), astRef);
1469
+ }
1470
+ for (const [id, statement] of graph.statementsById.entries()) {
1471
+ const root = statement.$document?.parseResult?.value;
1472
+ if (!root) continue;
1473
+ const path = astNodeLocator.getAstNodePath(statement);
1474
+ if (!path) continue;
1475
+ const uri = statement.$document?.uri?.toString() ?? graph.rootUri;
1476
+ map.set(normalizeDiagramElementId(id), { uri, path });
1477
+ }
1478
+ navigationIndex.set(graph.rootUri, map);
1479
+ }
1480
+
1481
+ export async function navigateToElement(shared: any, contextUri: string, elementId: string): Promise<RangeResponse | null> {
1482
+ const astNodeLocator = getAstNodeLocator(shared);
1483
+ if (!astNodeLocator) {
1484
+ return null;
1485
+ }
1486
+ const normalizedId = normalizeDiagramElementId(elementId);
1487
+ const directSeparator = normalizedId.indexOf('#');
1488
+ const directUri = directSeparator > 0 ? normalizedId.slice(0, directSeparator) : undefined;
1489
+ const directPath = directSeparator > 0 ? normalizedId.slice(directSeparator + 1) : undefined;
1490
+ const indexed = !directUri || !directPath
1491
+ ? navigationIndex.get(contextUri)?.get(normalizedId)
1492
+ : undefined;
1493
+ const resolvedUri = directUri ?? indexed?.uri;
1494
+ const resolvedPath = directPath ?? indexed?.path;
1495
+ if (!resolvedUri || !resolvedPath) {
1496
+ const ontologyIri = normalizedId.startsWith('ont:')
1497
+ ? normalizeNamespace(normalizedId.slice(4))
1498
+ : normalizeNamespace(normalizedId);
1499
+ if (ontologyIri && ontologyIri.includes('://')) {
1500
+ const ontology = await findOntologyByNamespace(shared, ontologyIri);
1501
+ if (ontology?.$document) {
1502
+ return toRange(ontology.$document, ontology);
1503
+ }
1504
+ }
1505
+ }
1506
+ if (!resolvedUri || !resolvedPath) {
1507
+ return null;
1508
+ }
1509
+ const langiumDocuments: any = shared.workspace.LangiumDocuments;
1510
+ const resolvedDocUri = URI.parse(resolvedUri);
1511
+ const doc = langiumDocuments.getDocument(resolvedDocUri)
1512
+ ?? (typeof langiumDocuments.getOrCreateDocument === 'function'
1513
+ ? await langiumDocuments.getOrCreateDocument(resolvedDocUri)
1514
+ : undefined);
1515
+ if (!doc) {
1516
+ return null;
1517
+ }
1518
+ const root = doc.parseResult?.value;
1519
+ if (!root) {
1520
+ return null;
1521
+ }
1522
+ const node = astNodeLocator.getAstNode(root, resolvedPath);
1523
+ if (!node) {
1524
+ return null;
1525
+ }
1526
+ return toRange(node.$document ?? doc, node);
1527
+ }
1528
+
1529
+ export async function resolveDefinition(shared: any, elementId: string, referencingUri?: string): Promise<RangeResponse | null> {
1530
+ const normalizedElementId = String(elementId ?? '').trim();
1531
+ if (!normalizedElementId) {
1532
+ return null;
1533
+ }
1534
+
1535
+ const directSeparator = normalizedElementId.indexOf('#');
1536
+ const directUri = directSeparator > 0 ? normalizedElementId.slice(0, directSeparator) : undefined;
1537
+ const directPath = directSeparator > 0 ? normalizedElementId.slice(directSeparator + 1) : undefined;
1538
+ if (directUri && directPath && directPath.startsWith('/')) {
1539
+ try {
1540
+ const langiumDocuments: any = shared.workspace.LangiumDocuments;
1541
+ const resolvedDocUri = URI.parse(directUri);
1542
+ const doc = langiumDocuments.getDocument(resolvedDocUri)
1543
+ ?? (typeof langiumDocuments.getOrCreateDocument === 'function'
1544
+ ? await langiumDocuments.getOrCreateDocument(resolvedDocUri)
1545
+ : undefined);
1546
+ const root = doc?.parseResult?.value;
1547
+ const astNodeLocator = getAstNodeLocator(shared);
1548
+ if (doc && root && astNodeLocator) {
1549
+ const node = astNodeLocator.getAstNode(root, directPath);
1550
+ if (node) {
1551
+ return toRange(node.$document ?? doc, node);
1552
+ }
1553
+ }
1554
+ } catch {
1555
+ // Fall through to IRI-based resolution when this is not a resolvable document URI.
1556
+ }
1557
+ }
1558
+
1559
+ const normalizedIri = normalizedElementId.replace(/^<|>$/g, '').trim();
1560
+ if (!normalizedIri) {
1561
+ return null;
1562
+ }
1563
+ if (referencingUri) {
1564
+ const preferredRange = await resolveDefinitionByIriInModel(shared, normalizedIri, referencingUri);
1565
+ if (preferredRange) {
1566
+ return preferredRange;
1567
+ }
1568
+ }
1569
+ const parts = splitIri(normalizedIri);
1570
+ if (!parts || !parts.base || !parts.fragment) {
1571
+ return null;
1572
+ }
1573
+ const ontology = await findOntologyByNamespace(shared, parts.base);
1574
+ if (!ontology?.$document) {
1575
+ return null;
1576
+ }
1577
+ const member = findOntologyMemberByName(ontology, parts.fragment);
1578
+ if (!member) {
1579
+ return null;
1580
+ }
1581
+ return toRange(member.$document ?? ontology.$document, member);
1582
+ }
1583
+
1584
+ async function resolveDefinitionByIriInModel(shared: any, iri: string, modelUri: string): Promise<RangeResponse | null> {
1585
+ const langiumDocuments: any = shared.workspace.LangiumDocuments;
1586
+ const builder: any = shared.workspace.DocumentBuilder;
1587
+ const uri = URI.parse(modelUri);
1588
+ const document = langiumDocuments.getDocument(uri)
1589
+ ?? (typeof langiumDocuments.getOrCreateDocument === 'function'
1590
+ ? await langiumDocuments.getOrCreateDocument(uri)
1591
+ : undefined);
1592
+ if (!document) {
1593
+ return null;
1594
+ }
1595
+ if (builder?.build) {
1596
+ await builder.build([document], { validation: false });
1597
+ }
1598
+ const root = document.parseResult?.value;
1599
+ if (!root || !isOntology(root) || (!isVocabulary(root) && !isDescription(root))) {
1600
+ return null;
1601
+ }
1602
+ const targetIri = normalizeIriText(iri);
1603
+ const member = collectOntologyMembers(root).find((candidate) => {
1604
+ const candidateIri = normalizeIriText(getIriForNode(candidate));
1605
+ if (candidateIri === targetIri) {
1606
+ return true;
1607
+ }
1608
+ const resolvedRefIri = normalizeIriText(getIriForNode((candidate as any)?.ref?.ref));
1609
+ if (resolvedRefIri === targetIri) {
1610
+ return true;
1611
+ }
1612
+ const rawRefText = normalizeIriText((candidate as any)?.ref?.$refText);
1613
+ return rawRefText === targetIri;
1614
+ });
1615
+ if (!member) {
1616
+ return null;
1617
+ }
1618
+ return toRange(member.$document ?? document, member);
1619
+ }
1620
+
1621
+ function normalizeIriText(value: unknown): string {
1622
+ return typeof value === 'string' ? value.replace(/^<|>$/g, '').trim() : '';
1623
+ }
1624
+
1625
+ async function findOntologyByNamespace(shared: any, namespace: string): Promise<Ontology | undefined> {
1626
+ const index = getOntologyModelIndex(shared);
1627
+ const modelUri = index?.resolveModelUri(normalizeNamespace(namespace));
1628
+ if (!modelUri) {
1629
+ return undefined;
1630
+ }
1631
+ const langiumDocuments: any = shared.workspace.LangiumDocuments;
1632
+ const uri = URI.parse(modelUri);
1633
+ const document = langiumDocuments.getDocument(uri)
1634
+ ?? (typeof langiumDocuments.getOrCreateDocument === 'function'
1635
+ ? await langiumDocuments.getOrCreateDocument(uri)
1636
+ : undefined);
1637
+ const root = document?.parseResult?.value;
1638
+ return root && isOntology(root) ? root : undefined;
1639
+ }
1640
+
1641
+ export type OntologyMemberLabelEntry = {
1642
+ iri: string;
1643
+ name: string;
1644
+ label?: string;
1645
+ };
1646
+
1647
+ export async function resolveOntologyMemberLabels(
1648
+ shared: any,
1649
+ oml: any,
1650
+ modelUri: string,
1651
+ ): Promise<OntologyMemberLabelEntry[]> {
1652
+ const langiumDocuments: any = shared.workspace.LangiumDocuments;
1653
+ const builder: any = shared.workspace.DocumentBuilder;
1654
+ const uri = URI.parse(modelUri);
1655
+ const document = langiumDocuments.getDocument(uri)
1656
+ ?? (typeof langiumDocuments.getOrCreateDocument === 'function'
1657
+ ? await langiumDocuments.getOrCreateDocument(uri)
1658
+ : undefined);
1659
+ if (!document) {
1660
+ return [];
1661
+ }
1662
+ if (builder?.build) {
1663
+ await builder.build([document], { validation: false });
1664
+ }
1665
+ const root = document.parseResult?.value;
1666
+ if (!root || !isOntology(root) || (!isVocabulary(root) && !isDescription(root))) {
1667
+ return [];
1668
+ }
1669
+ const reasoningService = oml?.reasoning?.ReasoningService;
1670
+ const labelSnapshot: Record<string, string> = reasoningService && typeof reasoningService.getContextMemberLabelSnapshot === 'function'
1671
+ ? await reasoningService.getContextMemberLabelSnapshot(modelUri)
1672
+ : {};
1673
+
1674
+ const entries: OntologyMemberLabelEntry[] = [];
1675
+ for (const member of collectOntologyMembers(root)) {
1676
+ const iri = getIriForNode(member)?.trim();
1677
+ const name = getNamedElementName(member)?.trim();
1678
+ if (!iri || !name) {
1679
+ continue;
1680
+ }
1681
+ const label = labelSnapshot[iri]?.trim();
1682
+ entries.push({
1683
+ iri,
1684
+ name,
1685
+ label: label && label.length > 0 ? label : undefined,
1686
+ });
1687
+ }
1688
+ entries.sort((left, right) =>
1689
+ left.name.localeCompare(right.name)
1690
+ || left.iri.localeCompare(right.iri)
1691
+ );
1692
+ return entries;
1693
+ }
1694
+
1695
+ function toRange(document: LangiumDocument | undefined, node: AstNode): RangeResponse | null {
1696
+ if (!document?.textDocument) return null;
1697
+ const cstNode = node.$cstNode;
1698
+ if (!cstNode) return null;
1699
+ const start = document.textDocument.positionAt(cstNode.offset);
1700
+ const end = document.textDocument.positionAt(cstNode.offset + cstNode.length);
1701
+ return {
1702
+ uri: document.uri.toString(),
1703
+ startLine: start.line + 1,
1704
+ startColumn: start.character,
1705
+ endLine: end.line + 1,
1706
+ endColumn: end.character
1707
+ };
1708
+ }