@likec4/language-server 1.15.0 → 1.16.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 (47) hide show
  1. package/contrib/likec4.tmLanguage.json +1 -1
  2. package/dist/browser.cjs +1 -1
  3. package/dist/browser.d.cts +2 -2
  4. package/dist/browser.d.mts +2 -2
  5. package/dist/browser.d.ts +2 -2
  6. package/dist/browser.mjs +2 -2
  7. package/dist/index.cjs +1 -1
  8. package/dist/index.d.cts +2 -2
  9. package/dist/index.d.mts +2 -2
  10. package/dist/index.d.ts +2 -2
  11. package/dist/index.mjs +2 -2
  12. package/dist/model-graph/index.cjs +1 -1
  13. package/dist/model-graph/index.d.cts +4 -2
  14. package/dist/model-graph/index.d.mts +4 -2
  15. package/dist/model-graph/index.d.ts +4 -2
  16. package/dist/model-graph/index.mjs +1 -1
  17. package/dist/shared/language-server.7iILaJYc.d.ts +1338 -0
  18. package/dist/shared/{language-server.80ITEDo5.cjs → language-server.Bd3NZ8uH.cjs} +130 -76
  19. package/dist/shared/{language-server.DXC9g4_f.mjs → language-server.C5gxpVUH.mjs} +132 -78
  20. package/dist/shared/language-server.CmBZHwSl.d.cts +1338 -0
  21. package/dist/shared/{language-server.U2piOAVt.d.cts → language-server.CnkCWVtf.d.cts} +44 -10
  22. package/dist/shared/{language-server.DfMwkd2l.d.mts → language-server.DIaiY0-C.d.mts} +44 -10
  23. package/dist/shared/language-server.DViE1Zxi.d.mts +1338 -0
  24. package/dist/shared/{language-server.zY53FGJE.mjs → language-server.D_13fWJQ.mjs} +216 -94
  25. package/dist/shared/{language-server.j-ShR6as.d.ts → language-server.DwyQ1FtY.d.ts} +44 -10
  26. package/dist/shared/{language-server.BUtiWTKg.cjs → language-server.Dym6GL4P.cjs} +214 -92
  27. package/package.json +8 -8
  28. package/src/ast.ts +11 -3
  29. package/src/formatting/LikeC4Formatter.ts +44 -4
  30. package/src/generated/ast.ts +111 -10
  31. package/src/generated/grammar.ts +1 -1
  32. package/src/like-c4.langium +19 -0
  33. package/src/lsp/SemanticTokenProvider.ts +3 -1
  34. package/src/model/model-builder.ts +34 -32
  35. package/src/model/model-parser.ts +91 -18
  36. package/src/model-graph/LikeC4ModelGraph.ts +22 -11
  37. package/src/model-graph/compute-view/__test__/fixture.ts +114 -44
  38. package/src/model-graph/compute-view/compute.ts +77 -72
  39. package/src/model-graph/dynamic-view/compute.ts +4 -1
  40. package/src/model-graph/utils/applyCustomRelationProperties.ts +7 -38
  41. package/src/model-graph/utils/applyViewRuleStyles.ts +0 -4
  42. package/src/model-graph/utils/relationExpressionToPredicates.ts +43 -0
  43. package/src/references/scope-computation.ts +10 -0
  44. package/src/validation/index.ts +3 -0
  45. package/src/validation/specification.ts +21 -0
  46. package/src/view-utils/index.ts +0 -1
  47. package/src/view-utils/resolve-global-rules.ts +66 -50
@@ -267,6 +267,7 @@ ViewLayoutDirection returns string:
267
267
 
268
268
  ViewRule:
269
269
  ViewRulePredicate |
270
+ ViewRuleGlobalPredicateRef |
270
271
  ViewRuleGroup |
271
272
  ViewRuleStyleOrGlobalRef |
272
273
  ViewRuleAutoLayout
@@ -287,6 +288,7 @@ ViewRuleGroup:
287
288
 
288
289
  DynamicViewRule:
289
290
  DynamicViewIncludePredicate |
291
+ DynamicViewGlobalPredicateRef |
290
292
  ViewRuleStyleOrGlobalRef |
291
293
  ViewRuleAutoLayout
292
294
  ;
@@ -318,6 +320,9 @@ Predicate:
318
320
  ElementPredicate
319
321
  ;
320
322
 
323
+ ViewRuleGlobalPredicateRef:
324
+ 'global' 'predicate' predicate=[GlobalPredicateGroup];
325
+
321
326
  ElementPredicate:
322
327
  ElementPredicates;
323
328
 
@@ -443,6 +448,9 @@ DynamicViewIncludePredicate:
443
448
  'include' predicates=DynamicViewPredicateIterator
444
449
  ;
445
450
 
451
+ DynamicViewGlobalPredicateRef:
452
+ 'global' 'predicate' predicate=[GlobalDynamicPredicateGroup];
453
+
446
454
  DynamicViewPredicateIterator:
447
455
  value=ElementPredicate ({infer DynamicViewPredicateIterator.prev=current} ',' (value=ElementPredicate)?)*
448
456
  ;
@@ -506,10 +514,21 @@ RelationNavigateToProperty:
506
514
  Globals:
507
515
  name='global' '{'
508
516
  (
517
+ predicates+=(GlobalPredicateGroup | GlobalDynamicPredicateGroup)*
509
518
  styles+=(GlobalStyle | GlobalStyleGroup)*
510
519
  )*
511
520
  '}';
512
521
 
522
+ GlobalPredicateGroup:
523
+ 'predicateGroup' name=IdTerminal '{'
524
+ predicates+=ViewRulePredicate*
525
+ '}';
526
+
527
+ GlobalDynamicPredicateGroup:
528
+ 'dynamicPredicateGroup' name=IdTerminal '{'
529
+ predicates+=DynamicViewIncludePredicate*
530
+ '}';
531
+
513
532
  GlobalStyleId:
514
533
  name=IdTerminal;
515
534
 
@@ -68,7 +68,7 @@ export class LikeC4SemanticTokenProvider extends AbstractSemanticTokenProvider {
68
68
  SemanticTokenModifiers.readonly
69
69
  ]
70
70
  })
71
- return
71
+ return 'prune'
72
72
  }
73
73
  if (ast.isWhereRelationKind(node) && isTruthy(node.value)) {
74
74
  acceptor({
@@ -97,6 +97,7 @@ export class LikeC4SemanticTokenProvider extends AbstractSemanticTokenProvider {
97
97
  type: SemanticTokenTypes.type,
98
98
  modifier: [SemanticTokenModifiers.definition]
99
99
  })
100
+ return 'prune'
100
101
  }
101
102
  if (ast.isElementTagExpression(node) && isTruthy(node.tag)) {
102
103
  acceptor({
@@ -105,6 +106,7 @@ export class LikeC4SemanticTokenProvider extends AbstractSemanticTokenProvider {
105
106
  type: SemanticTokenTypes.type,
106
107
  modifier: [SemanticTokenModifiers.definition]
107
108
  })
109
+ return 'prune'
108
110
  }
109
111
  if (ast.isElementRef(node) || ast.isFqnElementRef(node)) {
110
112
  acceptor({
@@ -13,10 +13,8 @@ import { deepEqual as eq } from 'fast-equals'
13
13
  import type { Cancellation, LangiumDocument, LangiumDocuments, URI, WorkspaceCache } from 'langium'
14
14
  import { Disposable, DocumentState, interruptAndCheck } from 'langium'
15
15
  import {
16
- entries,
17
16
  filter,
18
17
  flatMap,
19
- forEach,
20
18
  groupBy,
21
19
  indexBy,
22
20
  isEmpty,
@@ -46,21 +44,37 @@ import { isParsedLikeC4LangiumDocument } from '../ast'
46
44
  import { logError, logger, logWarnError } from '../logger'
47
45
  import { computeDynamicView, computeView, LikeC4ModelGraph } from '../model-graph'
48
46
  import type { LikeC4Services } from '../module'
49
- import { assignNavigateTo, resolveGlobalRules, resolveRelativePaths, resolveRulesExtendedViews } from '../view-utils'
47
+ import { assignNavigateTo, resolveRelativePaths, resolveRulesExtendedViews } from '../view-utils'
50
48
 
51
49
  function buildModel(services: LikeC4Services, docs: ParsedLikeC4LangiumDocument[]): c4.ParsedLikeC4Model {
50
+ // Merge specifications and globals from all documents
52
51
  const c4Specification: ParsedAstSpecification = {
53
52
  tags: new Set(),
54
53
  elements: {},
55
54
  relationships: {},
56
55
  colors: {}
57
56
  }
58
- forEach(map(docs, prop('c4Specification')), spec => {
57
+ const globals: c4.ModelGlobals = {
58
+ predicates: {},
59
+ dynamicPredicates: {},
60
+ styles: {}
61
+ }
62
+ for (const doc of docs) {
63
+ const {
64
+ c4Specification: spec,
65
+ c4Globals
66
+ } = doc
67
+
59
68
  spec.tags.forEach(t => c4Specification.tags.add(t))
60
69
  Object.assign(c4Specification.elements, spec.elements)
61
70
  Object.assign(c4Specification.relationships, spec.relationships)
62
71
  Object.assign(c4Specification.colors, spec.colors)
63
- })
72
+
73
+ Object.assign(globals.predicates, c4Globals.predicates)
74
+ Object.assign(globals.dynamicPredicates, c4Globals.dynamicPredicates)
75
+ Object.assign(globals.styles, c4Globals.styles)
76
+ }
77
+
64
78
  function resolveLinks(doc: LangiumDocument, links: c4.NonEmptyArray<ParsedLink>) {
65
79
  return map(
66
80
  links,
@@ -211,23 +225,6 @@ function buildModel(services: LikeC4Services, docs: ParsedLikeC4LangiumDocument[
211
225
  indexBy(prop('id'))
212
226
  )
213
227
 
214
- const parsedGlobals: {
215
- styles: Record<c4.GlobalStyleID, c4.ViewRuleStyle[]>
216
- } = {
217
- styles: {}
218
- }
219
- Object.assign(parsedGlobals.styles, ...docs.map(d => d.c4Globals.styles))
220
-
221
- const globals: {
222
- styles: Record<c4.GlobalStyleID, c4.GlobalStyle>
223
- } = {
224
- styles: pipe(
225
- entries(parsedGlobals.styles),
226
- map(([id, styles]) => ({ id, styles })),
227
- indexBy(prop('id'))
228
- )
229
- }
230
-
231
228
  function toC4View(doc: LangiumDocument) {
232
229
  const docUri = doc.uri.toString()
233
230
  return (parsedAstView: ParsedAstView): c4.LikeC4View => {
@@ -255,18 +252,23 @@ function buildModel(services: LikeC4Services, docs: ParsedLikeC4LangiumDocument[
255
252
 
256
253
  return {
257
254
  ...model,
255
+ customColorDefinitions,
258
256
  tags,
259
257
  links,
260
258
  docUri,
261
259
  description,
262
260
  title,
263
- id,
264
- customColorDefinitions
261
+ id
265
262
  }
266
263
  }
267
264
  }
268
265
 
269
- const parsedViews = docs.flatMap(d => map(d.c4Views, toC4View(d)))
266
+ const parsedViews = pipe(
267
+ docs,
268
+ flatMap(d => map(d.c4Views, toC4View(d))),
269
+ // Resolve relative paths and sort by
270
+ resolveRelativePaths
271
+ )
270
272
  // Add index view if not present
271
273
  if (!parsedViews.some(v => v.id === 'index')) {
272
274
  parsedViews.unshift({
@@ -291,7 +293,6 @@ function buildModel(services: LikeC4Services, docs: ParsedLikeC4LangiumDocument[
291
293
 
292
294
  const views = pipe(
293
295
  parsedViews,
294
- resolveRelativePaths,
295
296
  indexBy(prop('id')),
296
297
  resolveRulesExtendedViews
297
298
  )
@@ -362,7 +363,7 @@ export class LikeC4ModelBuilder {
362
363
  logger.debug('[ModelBuilder] No documents to build model from')
363
364
  return null
364
365
  }
365
- logger.debug(`[ModelBuilder] onValidated (${docs.length} docs)`)
366
+ logger.debug(`[ModelBuilder] buildModel (${docs.length} docs)`)
366
367
  return buildModel(this.services, docs)
367
368
  })
368
369
  }
@@ -395,7 +396,9 @@ export class LikeC4ModelBuilder {
395
396
 
396
397
  const allViews = [] as c4.ComputedView[]
397
398
  for (const view of values(model.views)) {
398
- const result = isElementView(view) ? computeView(view, index) : computeDynamicView(view, index)
399
+ const result = isElementView(view)
400
+ ? computeView(view, index)
401
+ : computeDynamicView(view, index)
399
402
  if (!result.isSuccess) {
400
403
  logWarnError(result.error)
401
404
  continue
@@ -460,10 +463,9 @@ export class LikeC4ModelBuilder {
460
463
  return null
461
464
  }
462
465
  const index = new LikeC4ModelGraph(model)
463
- const resolvedView = resolveGlobalRules(view, model.globals.styles)
464
- const result = isElementView(resolvedView)
465
- ? computeView(resolvedView, index)
466
- : computeDynamicView(resolvedView, index)
466
+ const result = isElementView(view)
467
+ ? computeView(view, index)
468
+ : computeDynamicView(view, index)
467
469
  if (!result.isSuccess) {
468
470
  logError(result.error)
469
471
  return null
@@ -11,6 +11,7 @@ import type {
11
11
  ParsedAstDynamicView,
12
12
  ParsedAstElement,
13
13
  ParsedAstElementView,
14
+ ParsedAstGlobals,
14
15
  ParsedAstRelation,
15
16
  ParsedLikeC4LangiumDocument,
16
17
  ParsedLink
@@ -288,6 +289,25 @@ export class LikeC4ModelParser {
288
289
  const { parseResult, c4Globals } = doc
289
290
 
290
291
  const globals = parseResult.value.globals.filter(isValid)
292
+
293
+ const elRelPredicates = globals.flatMap(r => r.predicates.filter(isValid))
294
+ for (const predicate of elRelPredicates) {
295
+ try {
296
+ const globalPredicateId = predicate.name as c4.GlobalPredicateId
297
+ if (!isTruthy(globalPredicateId)) {
298
+ continue
299
+ }
300
+ if (globalPredicateId in c4Globals.predicates) {
301
+ logger.warn(`Global predicate named "${globalPredicateId}" is already defined`)
302
+ continue
303
+ }
304
+
305
+ this.parseAndStoreGlobalPredicateGroupOrDynamic(predicate, globalPredicateId, c4Globals, isValid)
306
+ } catch (e) {
307
+ logWarnError(e)
308
+ }
309
+ }
310
+
291
311
  const styles = globals.flatMap(r => r.styles.filter(isValid))
292
312
  for (const style of styles) {
293
313
  try {
@@ -310,12 +330,49 @@ export class LikeC4ModelParser {
310
330
  }
311
331
  }
312
332
 
333
+ private parseAndStoreGlobalPredicateGroupOrDynamic(
334
+ astRule: ast.GlobalPredicateGroup | ast.GlobalDynamicPredicateGroup,
335
+ id: c4.GlobalPredicateId,
336
+ c4Globals: ParsedAstGlobals,
337
+ isValid: IsValidFn
338
+ ) {
339
+ if (ast.isGlobalPredicateGroup(astRule)) {
340
+ const predicates = this.parseGlobalPredicateGroup(astRule, isValid)
341
+ if (predicates.length > 0) {
342
+ c4Globals.predicates[id] = predicates as c4.NonEmptyArray<c4.ViewRulePredicate>
343
+ }
344
+ return
345
+ }
346
+ if (ast.isGlobalDynamicPredicateGroup(astRule)) {
347
+ const predicates = this.parseGlobalDynamicPredicateGroup(astRule, isValid)
348
+ if (predicates.length > 0) {
349
+ c4Globals.dynamicPredicates[id] = predicates as c4.NonEmptyArray<c4.DynamicViewIncludeRule>
350
+ }
351
+ return
352
+ }
353
+ nonexhaustive(astRule)
354
+ }
355
+
356
+ private parseGlobalPredicateGroup(
357
+ astRule: ast.GlobalPredicateGroup,
358
+ isValid: IsValidFn
359
+ ): c4.ViewRulePredicate[] {
360
+ return astRule.predicates.map(p => this.parseViewRulePredicate(p, isValid))
361
+ }
362
+
363
+ private parseGlobalDynamicPredicateGroup(
364
+ astRule: ast.GlobalDynamicPredicateGroup,
365
+ isValid: IsValidFn
366
+ ): c4.DynamicViewIncludeRule[] {
367
+ return astRule.predicates.map(p => this.parseDynamicViewIncludePredicate(p, isValid))
368
+ }
369
+
313
370
  private parseGlobalStyleOrGroup(
314
371
  astRule: ast.GlobalStyle | ast.GlobalStyleGroup,
315
372
  isValid: IsValidFn
316
373
  ): c4.ViewRuleStyle[] {
317
374
  if (ast.isGlobalStyle(astRule)) {
318
- return [this.parseGlobalStyle(astRule, isValid)]
375
+ return [this.parseViewRuleStyle(astRule, isValid)]
319
376
  }
320
377
  if (ast.isGlobalStyleGroup(astRule)) {
321
378
  return astRule.styles.map(s => this.parseViewRuleStyle(s, isValid))
@@ -323,19 +380,17 @@ export class LikeC4ModelParser {
323
380
  nonexhaustive(astRule)
324
381
  }
325
382
 
326
- private parseGlobalStyle(astRule: ast.GlobalStyle, isValid: IsValidFn): c4.ViewRuleStyle {
327
- const styleProps = astRule.props.filter(ast.isStyleProperty)
328
- const targets = astRule.target
329
- const notation = astRule.props.find(ast.isNotationProperty)
330
- return this.parseRuleStyle(styleProps, targets, isValid, notation)
331
- }
332
-
333
383
  private parseViews(doc: ParsedLikeC4LangiumDocument, isValid: IsValidFn) {
334
384
  const viewBlocks = doc.parseResult.value.views.filter(v => isValid(v))
335
385
  for (const viewBlock of viewBlocks) {
336
- const localStyles = viewBlock.styles
337
- .flatMap(s => this.parseViewRuleStyleOrGlobalRef(s, isValid))
338
- const stylesToApply = localStyles
386
+ const localStyles = viewBlock.styles.flatMap(s => {
387
+ try {
388
+ return this.parseViewRuleStyleOrGlobalRef(s, isValid)
389
+ } catch (e) {
390
+ logWarnError(e)
391
+ return []
392
+ }
393
+ })
339
394
 
340
395
  for (const view of viewBlock.views) {
341
396
  try {
@@ -344,8 +399,8 @@ export class LikeC4ModelParser {
344
399
  }
345
400
  doc.c4Views.push(
346
401
  ast.isElementView(view)
347
- ? this.parseElementView(view, stylesToApply, isValid)
348
- : this.parseDynamicElementView(view, stylesToApply, isValid)
402
+ ? this.parseElementView(view, localStyles, isValid)
403
+ : this.parseDynamicElementView(view, localStyles, isValid)
349
404
  )
350
405
  } catch (e) {
351
406
  logWarnError(e)
@@ -415,14 +470,14 @@ export class LikeC4ModelParser {
415
470
  }
416
471
  }
417
472
  if (ast.isElementKindExpression(astNode)) {
418
- invariant(astNode.kind, 'ElementKindExpr kind is not resolved: ' + astNode.$cstNode?.text)
473
+ invariant(astNode.kind?.ref, 'ElementKindExpr kind is not resolved: ' + astNode.$cstNode?.text)
419
474
  return {
420
- elementKind: astNode.kind.$refText as c4.ElementKind,
475
+ elementKind: astNode.kind.ref.name as c4.ElementKind,
421
476
  isEqual: astNode.isEqual
422
477
  }
423
478
  }
424
479
  if (ast.isElementTagExpression(astNode)) {
425
- invariant(astNode.tag, 'ElementTagExpr tag is not resolved: ' + astNode.$cstNode?.text)
480
+ invariant(astNode.tag?.ref, 'ElementTagExpr tag is not resolved: ' + astNode.$cstNode?.text)
426
481
  let elementTag = astNode.tag.$refText
427
482
  if (elementTag.startsWith('#')) {
428
483
  elementTag = elementTag.slice(1)
@@ -653,6 +708,9 @@ export class LikeC4ModelParser {
653
708
  if (ast.isViewRulePredicate(astRule)) {
654
709
  return this.parseViewRulePredicate(astRule, isValid)
655
710
  }
711
+ if (ast.isViewRuleGlobalPredicateRef(astRule)) {
712
+ return this.parseViewRuleGlobalPredicateRef(astRule, isValid)
713
+ }
656
714
  if (ast.isViewRuleStyleOrGlobalRef(astRule)) {
657
715
  return this.parseViewRuleStyleOrGlobalRef(astRule, isValid)
658
716
  }
@@ -665,7 +723,19 @@ export class LikeC4ModelParser {
665
723
  nonexhaustive(astRule)
666
724
  }
667
725
 
668
- private parseViewRuleStyleOrGlobalRef(astRule: ast.ViewRuleStyleOrGlobalRef, isValid: IsValidFn): c4.ViewRuleStyleOrGlobalRef {
726
+ private parseViewRuleGlobalPredicateRef(
727
+ astRule: ast.ViewRuleGlobalPredicateRef | ast.DynamicViewGlobalPredicateRef,
728
+ _isValid: IsValidFn
729
+ ): c4.ViewRuleGlobalPredicateRef {
730
+ return {
731
+ predicateId: astRule.predicate.$refText as c4.GlobalPredicateId
732
+ }
733
+ }
734
+
735
+ private parseViewRuleStyleOrGlobalRef(
736
+ astRule: ast.ViewRuleStyleOrGlobalRef,
737
+ isValid: IsValidFn
738
+ ): c4.ViewRuleStyleOrGlobalRef {
669
739
  if (ast.isViewRuleStyle(astRule)) {
670
740
  return this.parseViewRuleStyle(astRule, isValid)
671
741
  }
@@ -675,7 +745,7 @@ export class LikeC4ModelParser {
675
745
  nonexhaustive(astRule)
676
746
  }
677
747
 
678
- private parseViewRuleStyle(astRule: ast.ViewRuleStyle, isValid: IsValidFn): c4.ViewRuleStyle {
748
+ private parseViewRuleStyle(astRule: ast.ViewRuleStyle | ast.GlobalStyle, isValid: IsValidFn): c4.ViewRuleStyle {
679
749
  const styleProps = astRule.props.filter(ast.isStyleProperty)
680
750
  const targets = astRule.target
681
751
  const notation = astRule.props.find(ast.isNotationProperty)
@@ -971,6 +1041,9 @@ export class LikeC4ModelParser {
971
1041
  if (ast.isDynamicViewIncludePredicate(astRule)) {
972
1042
  return this.parseDynamicViewIncludePredicate(astRule, isValid)
973
1043
  }
1044
+ if (ast.isDynamicViewGlobalPredicateRef(astRule)) {
1045
+ return this.parseViewRuleGlobalPredicateRef(astRule, isValid)
1046
+ }
974
1047
  if (ast.isViewRuleStyleOrGlobalRef(astRule)) {
975
1048
  return this.parseViewRuleStyleOrGlobalRef(astRule, isValid)
976
1049
  }
@@ -5,6 +5,7 @@ import {
5
5
  type Fqn,
6
6
  invariant,
7
7
  isSameHierarchy,
8
+ type ModelGlobals,
8
9
  parentFqn,
9
10
  type Relation,
10
11
  type RelationID
@@ -14,7 +15,8 @@ import { isArray, isString } from 'remeda'
14
15
  type Params = {
15
16
  elements: Record<Fqn, Element>
16
17
  relations: Record<RelationID, Relation>
17
- // views: ElementView[]
18
+ // Optional for tests
19
+ globals?: ModelGlobals
18
20
  }
19
21
 
20
22
  type RelationEdge = {
@@ -41,24 +43,33 @@ function intersection<T>(a: Set<T>, b: Set<T>) {
41
43
  * Subject to change.
42
44
  */
43
45
  export class LikeC4ModelGraph {
44
- #elements = new Map<Fqn, Element>()
46
+ readonly #elements = new Map<Fqn, Element>()
45
47
  // Parent element for given FQN
46
- #parents = new Map<Fqn, Element>()
48
+ readonly #parents = new Map<Fqn, Element>()
47
49
  // Children elements for given FQN
48
- #children = new Map<Fqn, Element[]>()
49
- #rootElements = new Set<Element>()
50
+ readonly #children = new Map<Fqn, Element[]>()
51
+ readonly #rootElements = new Set<Element>()
50
52
 
51
- #relations = new Map<RelationID, Relation>()
53
+ readonly #relations = new Map<RelationID, Relation>()
52
54
  // Incoming to an element or its descendants
53
- #incoming = new MapRelations()
55
+ readonly #incoming = new MapRelations()
54
56
  // Outgoing from an element or its descendants
55
- #outgoing = new MapRelations()
57
+ readonly #outgoing = new MapRelations()
56
58
  // Relationships inside the element, among descendants
57
- #internal = new MapRelations()
59
+ readonly #internal = new MapRelations()
58
60
 
59
- #cacheAscendingSiblings = new Map<Fqn, Element[]>()
61
+ readonly #cacheAscendingSiblings = new Map<Fqn, Element[]>()
60
62
 
61
- constructor({ elements, relations }: Params) {
63
+ public readonly globals: ModelGlobals
64
+
65
+ constructor(
66
+ { elements, relations, globals }: Params
67
+ ) {
68
+ this.globals = globals ?? {
69
+ predicates: {},
70
+ dynamicPredicates: {},
71
+ styles: {}
72
+ }
62
73
  for (const el of Object.values(elements)) {
63
74
  this.addElement(el)
64
75
  }
@@ -11,12 +11,17 @@ import {
11
11
  type ElementWhereExpr,
12
12
  type Expression as C4Expression,
13
13
  type Fqn,
14
+ type GlobalStyleID,
15
+ type IncomingExpr as C4IncomingExpr,
16
+ type InOutExpr as C4InOutExpr,
14
17
  isElementRef,
15
18
  isElementWhere,
16
19
  isRelationExpression,
17
20
  isRelationWhere,
18
21
  type NonEmptyArray,
22
+ type OutgoingExpr as C4OutgoingExpr,
19
23
  type Relation,
24
+ type RelationExpr as C4RelationExpr,
20
25
  type RelationID,
21
26
  type RelationshipArrowType,
22
27
  type RelationshipLineType,
@@ -24,6 +29,7 @@ import {
24
29
  type Tag,
25
30
  type ViewID,
26
31
  type ViewRule,
32
+ type ViewRuleGlobalStyle,
27
33
  type ViewRuleGroup,
28
34
  type ViewRulePredicate,
29
35
  type ViewRuleStyle,
@@ -327,11 +333,37 @@ export const fakeRelations = [
327
333
  })
328
334
  ]
329
335
 
336
+ export const globalStyles = {
337
+ 'mute_old': [{
338
+ targets: [$expr({
339
+ elementTag: 'old' as Tag,
340
+ isEqual: true
341
+ })],
342
+ style: {
343
+ color: 'muted'
344
+ }
345
+ }],
346
+ 'red_next': [{
347
+ targets: [$expr({
348
+ elementTag: 'next' as Tag,
349
+ isEqual: true
350
+ })],
351
+ style: {
352
+ color: 'red'
353
+ }
354
+ }]
355
+ } as const
356
+
330
357
  export type FakeRelationIds = (typeof fakeRelations)[number]['id']
331
358
 
332
359
  export const fakeModel = new LikeC4ModelGraph({
333
360
  elements: fakeElements,
334
- relations: indexBy(fakeRelations, r => r.id)
361
+ relations: indexBy(fakeRelations, r => r.id),
362
+ globals: {
363
+ predicates: {},
364
+ dynamicPredicates: {},
365
+ styles: globalStyles
366
+ }
335
367
  })
336
368
 
337
369
  const emptyView = {
@@ -414,6 +446,49 @@ export function $where(
414
446
  }
415
447
  }
416
448
 
449
+ export function $inout(
450
+ expr: InOutExpr | C4ElementExpression
451
+ ): C4InOutExpr {
452
+ const innerExpression = !isString(expr)
453
+ ? expr as C4Expression
454
+ : $expr(expr.replace(/->/g, '').trim() as ElementRefExpr) as any
455
+
456
+ return { inout: innerExpression }
457
+ }
458
+
459
+ export function $incoming(
460
+ expr: IncomingExpr | C4ElementExpression
461
+ ): C4IncomingExpr {
462
+ const innerExpression = !isString(expr)
463
+ ? expr as C4Expression
464
+ : $expr(expr.replace('-> ', '') as ElementRefExpr) as any
465
+
466
+ return { incoming: innerExpression }
467
+ }
468
+
469
+ export function $outgoing(
470
+ expr: OutgoingExpr | C4ElementExpression
471
+ ): C4OutgoingExpr {
472
+ const innerExpression = !isString(expr)
473
+ ? expr as C4Expression
474
+ : $expr(expr.replace(' ->', '') as ElementRefExpr) as any
475
+
476
+ return { outgoing: innerExpression }
477
+ }
478
+
479
+ export function $relation(
480
+ expr: RelationExpr
481
+ ): C4RelationExpr {
482
+ const [source, target] = expr.split(/ -> | <-> /)
483
+ const isBidirectional = expr.includes(' <-> ')
484
+
485
+ return {
486
+ source: $expr(source as ElementRefExpr) as any,
487
+ target: $expr(target as ElementRefExpr) as any,
488
+ ...(isBidirectional && { isBidirectional })
489
+ }
490
+ }
491
+
417
492
  export function $expr(expr: Expression | C4Expression): C4Expression {
418
493
  if (!isString(expr)) {
419
494
  return expr as C4Expression
@@ -422,34 +497,13 @@ export function $expr(expr: Expression | C4Expression): C4Expression {
422
497
  return { wildcard: true }
423
498
  }
424
499
  if (expr.startsWith('->')) {
425
- if (expr.endsWith('->')) {
426
- return {
427
- inout: $expr(expr.replace(/->/g, '').trim() as ElementRefExpr) as any
428
- }
429
- }
430
- return {
431
- incoming: $expr(expr.replace('-> ', '') as ElementRefExpr) as any
432
- }
500
+ return expr.endsWith('->') ? $inout(expr as InOutExpr) : $incoming(expr as IncomingExpr)
433
501
  }
434
502
  if (expr.endsWith(' ->')) {
435
- return {
436
- outgoing: $expr(expr.replace(' ->', '') as ElementRefExpr) as any
437
- }
438
- }
439
- if (expr.includes(' <-> ')) {
440
- const [source, target] = expr.split(' <-> ')
441
- return {
442
- source: $expr(source as ElementRefExpr) as any,
443
- target: $expr(target as ElementRefExpr) as any,
444
- isBidirectional: true
445
- }
503
+ return $outgoing(expr as OutgoingExpr)
446
504
  }
447
- if (expr.includes(' -> ')) {
448
- const [source, target] = expr.split(' -> ')
449
- return {
450
- source: $expr(source as ElementRefExpr) as any,
451
- target: $expr(target as ElementRefExpr) as any
452
- }
505
+ if (expr.includes(' -> ') || expr.includes(' <-> ')) {
506
+ return $relation(expr as RelationExpr)
453
507
  }
454
508
  if (expr.endsWith('._')) {
455
509
  return {
@@ -484,30 +538,34 @@ type CustomProps = {
484
538
  }
485
539
  export function $include(expr: Expression | C4Expression, props?: CustomProps): ViewRulePredicate {
486
540
  let _expr = props?.where ? $where(expr, props.where) : $expr(expr)
487
- if (props?.with) {
488
- if (isRelationExpression(_expr) || isRelationWhere(_expr)) {
489
- _expr = {
490
- customRelation: {
491
- relation: _expr,
492
- ...props.with as any
493
- }
541
+ _expr = props?.with ? $with(_expr, props.with) : _expr
542
+ return {
543
+ include: [_expr]
544
+ }
545
+ }
546
+ export function $with(expr: C4Expression, props?: CustomProps['with']): C4CustomRelationExpr | C4CustomElementExpr {
547
+ if (isRelationExpression(expr) || isRelationWhere(expr)) {
548
+ return {
549
+ customRelation: {
550
+ relation: expr,
551
+ ...props as any
494
552
  }
495
- } else if (isElementRef(_expr) || isElementWhere(_expr)) {
496
- _expr = {
497
- custom: {
498
- expr: _expr,
499
- ...props.with as any
500
- }
553
+ }
554
+ } else if (isElementRef(expr) || isElementWhere(expr)) {
555
+ return {
556
+ custom: {
557
+ expr: expr,
558
+ ...props as any
501
559
  }
502
560
  }
503
561
  }
504
- return {
505
- include: [_expr]
506
- }
562
+
563
+ throw 'Unsupported type of internal expression'
507
564
  }
508
- export function $exclude(expr: Expression | C4Expression): ViewRulePredicate {
565
+ export function $exclude(expr: Expression | C4Expression, where?: WhereOperator<TestTag, string>): ViewRulePredicate {
566
+ let _expr = where ? $where(expr, where) : $expr(expr)
509
567
  return {
510
- exclude: [$expr(expr)]
568
+ exclude: [_expr]
511
569
  }
512
570
  }
513
571
  export function $group(groupRules: ViewRuleGroup['groupRules']): ViewRuleGroup {
@@ -524,6 +582,18 @@ export function $style(element: ElementRefExpr, style: ViewRuleStyle['style']):
524
582
  }
525
583
  }
526
584
 
585
+ type GlobalStyles = keyof typeof globalStyles
586
+ type GlobalExpr = `style ${GlobalStyles}`
587
+ export function $global(expr: GlobalExpr): ViewRuleGlobalStyle {
588
+ const [_t, id] = expr.split(' ') as [string, string]
589
+ if (_t === 'style') {
590
+ return {
591
+ styleId: id as GlobalStyleID
592
+ }
593
+ }
594
+ throw new Error('Invalid global expression')
595
+ }
596
+
527
597
  export function computeView(
528
598
  ...args: [FakeElementIds, ViewRule | ViewRule[]] | [ViewRule | ViewRule[]]
529
599
  ) {